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 generateCreate 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-idAlways 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
- Next.js Integration -- Server-side rendering with Next.js App Router
- Queries Reference -- Full API for
findMany(),findById(), andcount() - Client Overview -- Architecture of the generated client