- AI
- Analytics
- Auth
- CMS
- Collaboration
- Cron Jobs
- Database
- Data Layer
- CRUD System
- Design System
- Transactional Emails
- Feature Flags
- Formatting
- Internationalization
- Observability
- Next.js Config
- Notifications
- Payments
- Security
- SEO
- Storage
- Testing
- Toolbar
- View System
- View Builder
- Trigger
- Trigger Rules
- Webhooks
CRUD Hooks
React hooks for CRUD operations
CRUD Hooks
The hooks package in @repo/crud
provides a collection of React hooks for performing CRUD operations in your components. These hooks are built on top of the CRUD Engine and provide a seamless integration with React.
Features
- Data Fetching: Hooks for fetching data from your data provider
- Data Mutations: Hooks for creating, updating, and deleting data
- Loading States: Automatic handling of loading states
- Error Handling: Consistent error handling across hooks
- Optimistic Updates: Support for optimistic UI updates
- Refetching: Automatic and manual refetching of data
- TypeScript Support: Full TypeScript support with generics
Installation
This package is part of the zopio
monorepo and is available to all applications in the workspace.
# If you're adding it to a new package in the monorepo
pnpm add @repo/crud
Basic Usage
Setting Up the Provider
Before using the hooks, you need to set up the CRUD provider in your application:
import { ui } from '@repo/crud';
import { engine } from '@repo/crud';
import { providers } from '@repo/data';
// Create a data provider
const dataProvider = providers.supabase.createProvider({
url: process.env.SUPABASE_URL,
key: process.env.SUPABASE_KEY
});
// Create a CRUD engine
const crudEngine = engine.createCrudEngine({
dataProvider,
enableAudit: true,
enablePermissions: true
});
// Wrap your application with the CRUD provider
function App() {
return (
<ui.CrudProvider engine={crudEngine}>
<YourApp />
</ui.CrudProvider>
);
}
Data Fetching Hooks
useGetList
The useGetList
hook fetches a list of resources from your data provider.
import { hooks } from '@repo/crud';
function UserList() {
const {
data,
total,
loading,
error,
refetch,
setParams
} = hooks.useGetList('users', {
pagination: { page: 1, perPage: 10 },
sort: { field: 'createdAt', order: 'desc' },
filter: { role: 'admin' }
});
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<button onClick={() => refetch()}>Refresh</button>
<button onClick={() => setParams(prev => ({
...prev,
pagination: { page: prev.pagination.page + 1, perPage: prev.pagination.perPage }
}))}>
Next Page
</button>
<p>Total: {total}</p>
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
Options
interface UseGetListOptions {
pagination?: {
page: number;
perPage: number;
};
sort?: {
field: string;
order: 'asc' | 'desc';
};
filter?: Record<string, any>;
enabled?: boolean;
refetchInterval?: number;
refetchOnWindowFocus?: boolean;
onSuccess?: (data: any[], total: number) => void;
onError?: (error: Error) => void;
}
useGetOne
The useGetOne
hook fetches a single resource from your data provider.
import { hooks } from '@repo/crud';
function UserDetails({ id }) {
const {
data,
loading,
error,
refetch
} = hooks.useGetOne('users', id);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<button onClick={() => refetch()}>Refresh</button>
<h2>{data.name}</h2>
<p>Email: {data.email}</p>
<p>Role: {data.role}</p>
</div>
);
}
Options
interface UseGetOneOptions {
enabled?: boolean;
refetchInterval?: number;
refetchOnWindowFocus?: boolean;
onSuccess?: (data: any) => void;
onError?: (error: Error) => void;
}
useGetMany
The useGetMany
hook fetches multiple resources by their IDs.
import { hooks } from '@repo/crud';
function UserList({ userIds }) {
const {
data,
loading,
error,
refetch
} = hooks.useGetMany('users', userIds);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<button onClick={() => refetch()}>Refresh</button>
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
Options
interface UseGetManyOptions {
enabled?: boolean;
refetchInterval?: number;
refetchOnWindowFocus?: boolean;
onSuccess?: (data: any[]) => void;
onError?: (error: Error) => void;
}
Data Mutation Hooks
useCreate
The useCreate
hook creates a new resource in your data provider.
import { hooks } from '@repo/crud';
function CreateUser() {
const [create, { loading, error }] = hooks.useCreate('users');
const handleSubmit = async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const data = {
name: formData.get('name'),
email: formData.get('email'),
role: formData.get('role')
};
try {
const result = await create(data);
console.log('Created user:', result.data);
} catch (err) {
console.error('Failed to create user:', err);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="name">Name</label>
<input type="text" id="name" name="name" required />
</div>
<div>
<label htmlFor="email">Email</label>
<input type="email" id="email" name="email" required />
</div>
<div>
<label htmlFor="role">Role</label>
<select id="role" name="role">
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
</div>
<button type="submit" disabled={loading}>
{loading ? 'Creating...' : 'Create User'}
</button>
{error && <div>Error: {error.message}</div>}
</form>
);
}
Options
interface UseCreateOptions {
onSuccess?: (data: any) => void;
onError?: (error: Error) => void;
invalidateQueries?: boolean | string[];
}
useUpdate
The useUpdate
hook updates an existing resource in your data provider.
import { hooks } from '@repo/crud';
function UpdateUser({ id }) {
const { data, loading: fetchLoading } = hooks.useGetOne('users', id);
const [update, { loading: updateLoading, error }] = hooks.useUpdate('users');
const handleSubmit = async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const data = {
name: formData.get('name'),
email: formData.get('email'),
role: formData.get('role')
};
try {
const result = await update(id, data);
console.log('Updated user:', result.data);
} catch (err) {
console.error('Failed to update user:', err);
}
};
if (fetchLoading) return <div>Loading...</div>;
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="name">Name</label>
<input type="text" id="name" name="name" defaultValue={data.name} required />
</div>
<div>
<label htmlFor="email">Email</label>
<input type="email" id="email" name="email" defaultValue={data.email} required />
</div>
<div>
<label htmlFor="role">Role</label>
<select id="role" name="role" defaultValue={data.role}>
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
</div>
<button type="submit" disabled={updateLoading}>
{updateLoading ? 'Updating...' : 'Update User'}
</button>
{error && <div>Error: {error.message}</div>}
</form>
);
}
Options
interface UseUpdateOptions {
onSuccess?: (data: any) => void;
onError?: (error: Error) => void;
invalidateQueries?: boolean | string[];
optimistic?: boolean;
}
useDelete
The useDelete
hook deletes an existing resource from your data provider.
import { hooks } from '@repo/crud';
function DeleteUser({ id, onDeleted }) {
const [deleteUser, { loading, error }] = hooks.useDelete('users');
const handleDelete = async () => {
if (window.confirm('Are you sure you want to delete this user?')) {
try {
await deleteUser(id);
onDeleted();
} catch (err) {
console.error('Failed to delete user:', err);
}
}
};
return (
<div>
<button onClick={handleDelete} disabled={loading}>
{loading ? 'Deleting...' : 'Delete User'}
</button>
{error && <div>Error: {error.message}</div>}
</div>
);
}
Options
interface UseDeleteOptions {
onSuccess?: (data: any) => void;
onError?: (error: Error) => void;
invalidateQueries?: boolean | string[];
optimistic?: boolean;
}
Advanced Hooks
useMutation
The useMutation
hook provides a more flexible way to perform mutations.
import { hooks } from '@repo/crud';
function ToggleUserStatus({ id, status, onToggled }) {
const { mutate, loading, error } = hooks.useMutation({
mutationFn: async () => {
const newStatus = status === 'active' ? 'inactive' : 'active';
const { data } = await crudEngine.update({
resource: 'users',
id,
data: { status: newStatus }
});
return data;
},
onSuccess: (data) => {
onToggled(data.status);
},
// Optimistic update
optimisticUpdate: {
resource: 'users',
id,
data: { status: status === 'active' ? 'inactive' : 'active' }
}
});
return (
<button onClick={mutate} disabled={loading}>
{loading ? 'Updating...' : `Mark as ${status === 'active' ? 'Inactive' : 'Active'}`}
</button>
);
}
Options
interface UseMutationOptions {
mutationFn: () => Promise<any>;
onSuccess?: (data: any) => void;
onError?: (error: Error) => void;
invalidateQueries?: boolean | string[];
optimisticUpdate?: {
resource: string;
id?: string | number;
data: Record<string, any>;
};
}
useQueryClient
The useQueryClient
hook provides access to the query client for advanced use cases.
import { hooks } from '@repo/crud';
function InvalidateCache() {
const queryClient = hooks.useQueryClient();
const handleInvalidateUsers = () => {
queryClient.invalidateQueries(['users']);
};
const handleInvalidateAll = () => {
queryClient.invalidateQueries();
};
return (
<div>
<button onClick={handleInvalidateUsers}>Invalidate Users Cache</button>
<button onClick={handleInvalidateAll}>Invalidate All Cache</button>
</div>
);
}
useCrudEngine
The useCrudEngine
hook provides direct access to the CRUD engine.
import { hooks } from '@repo/crud';
function CustomOperation() {
const crudEngine = hooks.useCrudEngine();
const handleCustomOperation = async () => {
// Perform a custom operation using the CRUD engine
const result = await crudEngine.getList({
resource: 'users',
pagination: { page: 1, perPage: 100 },
filter: { active: true }
});
// Process the result
const activeUserCount = result.total;
console.log(`Active users: ${activeUserCount}`);
};
return (
<button onClick={handleCustomOperation}>
Count Active Users
</button>
);
}
Optimistic Updates
Many of the hooks support optimistic updates, which update the UI immediately before the server request completes:
import { hooks } from '@repo/crud';
function UserStatusToggle({ id, status }) {
const [update, { loading }] = hooks.useUpdate('users', {
optimistic: true
});
const handleToggle = async () => {
const newStatus = status === 'active' ? 'inactive' : 'active';
await update(id, { status: newStatus });
};
return (
<button onClick={handleToggle} disabled={loading}>
{status === 'active' ? 'Deactivate' : 'Activate'}
</button>
);
}
TypeScript Support
All hooks are fully typed and support generics for better type safety:
import { hooks } from '@repo/crud';
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
status: 'active' | 'inactive';
}
function UserList() {
const { data, loading, error } = hooks.useGetList<User>('users');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{data.map(user => (
<li key={user.id}>
{user.name} - {user.role} - {user.status}
</li>
))}
</ul>
);
}
Custom Hooks
You can create custom hooks that build on the provided hooks:
import { hooks } from '@repo/crud';
// Custom hook for user management
function useUsers() {
const { data, loading, error, refetch } = hooks.useGetList('users');
const [createUser] = hooks.useCreate('users');
const [updateUser] = hooks.useUpdate('users');
const [deleteUser] = hooks.useDelete('users');
const addUser = async (userData) => {
await createUser(userData);
refetch();
};
const editUser = async (id, userData) => {
await updateUser(id, userData);
refetch();
};
const removeUser = async (id) => {
await deleteUser(id);
refetch();
};
return {
users: data,
loading,
error,
addUser,
editUser,
removeUser
};
}
// Usage
function UserManagement() {
const { users, loading, error, addUser, editUser, removeUser } = useUsers();
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h2>Users</h2>
<ul>
{users.map(user => (
<li key={user.id}>
{user.name}
<button onClick={() => editUser(user.id, { role: 'admin' })}>
Make Admin
</button>
<button onClick={() => removeUser(user.id)}>
Delete
</button>
</li>
))}
</ul>
<button onClick={() => addUser({ name: 'New User', role: 'user' })}>
Add User
</button>
</div>
);
}
Pagination Hook
A custom hook for handling pagination:
import { hooks } from '@repo/crud';
import { useState } from 'react';
function usePagination(resource, perPage = 10) {
const [page, setPage] = useState(1);
const { data, total, loading, error } = hooks.useGetList(resource, {
pagination: { page, perPage }
});
const totalPages = Math.ceil(total / perPage);
const hasNextPage = page < totalPages;
const hasPrevPage = page > 1;
const nextPage = () => {
if (hasNextPage) {
setPage(prev => prev + 1);
}
};
const prevPage = () => {
if (hasPrevPage) {
setPage(prev => prev - 1);
}
};
const goToPage = (pageNumber) => {
if (pageNumber >= 1 && pageNumber <= totalPages) {
setPage(pageNumber);
}
};
return {
data,
total,
loading,
error,
page,
perPage,
totalPages,
hasNextPage,
hasPrevPage,
nextPage,
prevPage,
goToPage
};
}
// Usage
function PaginatedUserList() {
const {
data,
loading,
error,
page,
totalPages,
hasNextPage,
hasPrevPage,
nextPage,
prevPage,
goToPage
} = usePagination('users', 5);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
<div>
<button onClick={prevPage} disabled={!hasPrevPage}>
Previous
</button>
<span>
Page {page} of {totalPages}
</span>
<button onClick={nextPage} disabled={!hasNextPage}>
Next
</button>
</div>
</div>
);
}
API Reference
For a complete API reference, please refer to the TypeScript definitions in the package.
- CRUD Hooks
- Features
- Installation
- Basic Usage
- Setting Up the Provider
- Data Fetching Hooks
- useGetList
- Options
- useGetOne
- Options
- useGetMany
- Options
- Data Mutation Hooks
- useCreate
- Options
- useUpdate
- Options
- useDelete
- Options
- Advanced Hooks
- useMutation
- Options
- useQueryClient
- useCrudEngine
- Optimistic Updates
- TypeScript Support
- Custom Hooks
- Pagination Hook
- API Reference