Laizy CMS
Integrations

React

Integrate the Laizy CMS generated client with React applications using Vite, Remix, or other frameworks.

React Integration

The Laizy generated client works with any React application -- Vite, Remix, Create React App, or any other setup. This guide covers client-side patterns for fetching and displaying content.

Setup

Generate the client

Initialize your Laizy project and generate the TypeScript client:

pnpm laizy init
pnpm laizy sync
pnpm laizy generate

Create the CMS client

Set up the Laizy client with your frontend token:

// lib/cms.ts
import { LaizyClient } from './generated/laizy';
import { ManagementClient } from '@/lib/management-client';

const managementClient = new ManagementClient({
  baseUrl: import.meta.env.VITE_LAIZY_BASE_URL,
  apiToken: import.meta.env.VITE_LAIZY_TOKEN,
  projectId: import.meta.env.VITE_LAIZY_PROJECT_ID,
});

export const cms = new LaizyClient(managementClient);

Set environment variables

Add your Laizy configuration to your .env file:

VITE_LAIZY_BASE_URL=https://laizycms.com
VITE_LAIZY_TOKEN=laizy_eyJhbG...
VITE_LAIZY_PROJECT_ID=your-project-id

Always use frontend tokens (scope: content:read) in client-side React code. These tokens can only read published content and are safe to expose in the browser. Never use admin tokens in client-side bundles.

Basic Usage

Fetching Content with useEffect

The simplest pattern for loading content in a React component:

import { useEffect, useState } from 'react';
import type { BlogPost } from './generated/laizy';
import { cms } from './lib/cms';

function BlogList() {
  const [posts, setPosts] = useState<BlogPost[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    cms.blogPost.findMany({ limit: 20 })
      .then(setPosts)
      .catch((err) => setError(err.message))
      .finally(() => setLoading(false));
  }, []);

  if (loading) return <p>Loading posts...</p>;
  if (error) return <p>Error: {error}</p>;
  if (posts.length === 0) return <p>No posts found.</p>;

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.content}</p>
          <time>{new Date(post.createdAt).toLocaleDateString()}</time>
        </li>
      ))}
    </ul>
  );
}

Fetching a Single Entry

import { useEffect, useState } from 'react';
import type { BlogPost } from './generated/laizy';
import { cms } from './lib/cms';

function BlogPostDetail({ postId }: { postId: string }) {
  const [post, setPost] = useState<BlogPost | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    cms.blogPost.findById(postId)
      .then(setPost)
      .finally(() => setLoading(false));
  }, [postId]);

  if (loading) return <p>Loading...</p>;
  if (!post) return <p>Post not found.</p>;

  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  );
}

Custom Hook Pattern

Extract content fetching into reusable hooks for cleaner components:

// hooks/use-cms.ts
import { useEffect, useState } from 'react';
import type { BlogPost, Author } from './generated/laizy';
import { cms } from './lib/cms';

export function useBlogPosts(limit = 20) {
  const [posts, setPosts] = useState<BlogPost[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    cms.blogPost.findMany({ limit })
      .then(setPosts)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [limit]);

  return { posts, loading, error };
}

export function useBlogPost(id: string) {
  const [post, setPost] = useState<BlogPost | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    cms.blogPost.findById(id)
      .then(setPost)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [id]);

  return { post, loading, error };
}

export function useAuthors() {
  const [authors, setAuthors] = useState<Author[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    cms.author.findMany()
      .then(setAuthors)
      .finally(() => setLoading(false));
  }, []);

  return { authors, loading };
}

Use the hooks in components:

function BlogPage() {
  const { posts, loading, error } = useBlogPosts(10);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error loading posts.</p>;

  return (
    <div>
      <h1>Blog</h1>
      {posts.map((post) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.content}</p>
        </article>
      ))}
    </div>
  );
}

Pagination

Implement pagination with offset-based queries:

import { useState, useEffect } from 'react';
import type { BlogPost } from './generated/laizy';
import { cms } from './lib/cms';

const PAGE_SIZE = 10;

function PaginatedBlog() {
  const [posts, setPosts] = useState<BlogPost[]>([]);
  const [total, setTotal] = useState(0);
  const [page, setPage] = useState(1);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    setLoading(true);
    const offset = (page - 1) * PAGE_SIZE;

    Promise.all([
      cms.blogPost.findMany({ limit: PAGE_SIZE, offset }),
      cms.blogPost.count(),
    ])
      .then(([fetchedPosts, count]) => {
        setPosts(fetchedPosts);
        setTotal(count);
      })
      .finally(() => setLoading(false));
  }, [page]);

  const totalPages = Math.ceil(total / PAGE_SIZE);

  return (
    <div>
      {loading ? (
        <p>Loading...</p>
      ) : (
        <>
          {posts.map((post) => (
            <article key={post.id}>
              <h2>{post.title}</h2>
            </article>
          ))}
          <div>
            <button
              disabled={page <= 1}
              onClick={() => setPage(page - 1)}
            >
              Previous
            </button>
            <span>Page {page} of {totalPages}</span>
            <button
              disabled={page >= totalPages}
              onClick={() => setPage(page + 1)}
            >
              Next
            </button>
          </div>
        </>
      )}
    </div>
  );
}

With React Query / TanStack Query

For more advanced caching, refetching, and state management, pair the Laizy client with TanStack Query:

import { useQuery } from '@tanstack/react-query';
import { cms } from './lib/cms';

function BlogList() {
  const { data: posts, isLoading, error } = useQuery({
    queryKey: ['blogPosts'],
    queryFn: () => cms.blogPost.findMany({ limit: 20 }),
  });

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error loading posts.</p>;

  return (
    <ul>
      {posts?.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

function BlogPost({ id }: { id: string }) {
  const { data: post, isLoading } = useQuery({
    queryKey: ['blogPost', id],
    queryFn: () => cms.blogPost.findById(id),
  });

  if (isLoading) return <p>Loading...</p>;
  if (!post) return <p>Not found.</p>;

  return <h1>{post.title}</h1>;
}

Error Handling

Always handle errors gracefully in client-side code:

function SafeBlogList() {
  const { posts, loading, error } = useBlogPosts();

  if (loading) {
    return <div className="skeleton">Loading content...</div>;
  }

  if (error) {
    return (
      <div className="error">
        <p>Unable to load content. Please try again later.</p>
        <button onClick={() => window.location.reload()}>
          Retry
        </button>
      </div>
    );
  }

  if (posts.length === 0) {
    return <p>No posts published yet.</p>;
  }

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

Next Steps

On this page