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.