# @lilith/react-query-utils Shared React Query utilities for the lilith platform monorepo. ## Features - **CRUD Hook Generators** - Reduces boilerplate by 60-70% - **Paginated Query Hooks** - Built-in pagination controls - **Standardized Mutation Options** - Automatic error handling and query invalidation - **Optimistic Updates** - Optional optimistic UI updates ## Installation ```bash pnpm add @lilith/react-query-utils ``` ## Usage ### useMutationOptions Create standardized mutation options with automatic error handling, toast notifications, and query invalidation. #### Basic Usage ```typescript import { useMutation } from '@tanstack/react-query'; import { useMutationOptions } from '@lilith/react-query-utils'; import { apiClient } from './api'; function useCreateUser() { const options = useMutationOptions({ operation: 'create user', invalidateKeys: [['users']], }); return useMutation({ mutationFn: (data) => apiClient.post('/users', data), ...options, }); } // In component function CreateUserForm() { const { mutate: createUser, isPending } = useCreateUser(); const handleSubmit = (data) => { createUser(data); // Automatically shows success toast and invalidates users query }; } ``` #### Custom Success Messages ```typescript // Default message (capitalized operation name) const options = useMutationOptions({ operation: 'create user', // Shows "Create user successful" }); // Custom message const options = useMutationOptions({ operation: 'create user', successMessage: 'User created successfully!', }); // Disable success message const options = useMutationOptions({ operation: 'update settings', successMessage: false, // No toast on success }); ``` #### Query Invalidation ```typescript // Single query key const options = useMutationOptions({ operation: 'create user', invalidateKeys: [['users']], // Invalidates ['users'] query }); // Multiple query keys const options = useMutationOptions({ operation: 'delete post', invalidateKeys: [ ['posts'], // All posts ['posts', postId], // Specific post ['user', userId, 'posts'], // User's posts ], }); // No invalidation const options = useMutationOptions({ operation: 'update settings', // Don't specify invalidateKeys }); ``` #### Custom Callbacks ```typescript const options = useMutationOptions({ operation: 'create user', successMessage: false, invalidateKeys: [['users']], onSuccess: (user) => { console.log('Created user:', user); navigate(`/users/${user.id}`); }, onError: (error) => { // Additional error handling beyond toast logErrorToService(error); }, }); ``` #### Error Logging Control ```typescript // Error logging enabled by default const options = useMutationOptions({ operation: 'create user', // Logs errors to console automatically }); // Disable error logging const options = useMutationOptions({ operation: 'create user', enableErrorLogging: false, }); ``` #### Before/After Comparison **Before** (manual error handling): ```typescript function useCreateUser() { const queryClient = useQueryClient(); return useMutation({ mutationFn: (data) => apiClient.post('/users', data), onSuccess: (user) => { toast.success('User created successfully!'); queryClient.invalidateQueries({ queryKey: ['users'] }); }, onError: (error) => { const message = getErrorMessage(error); toast.error(message || 'Failed to create user'); console.error('[create user] Error:', error); }, }); } ``` **After** (using useMutationOptions): ```typescript function useCreateUser() { const options = useMutationOptions({ operation: 'create user', successMessage: 'User created successfully!', invalidateKeys: [['users']], }); return useMutation({ mutationFn: (data) => apiClient.post('/users', data), ...options, }); } ``` **Savings**: ~60% less boilerplate, standardized error handling across the app. ### createCrudHooks Generate a complete set of CRUD hooks for an entity. #### Basic Usage ```typescript import { createCrudHooks } from '@lilith/react-query-utils'; import { userApi } from './api/users'; // Generate hooks const { useGetAll, useGetById, useCreate, useUpdate, useDelete, } = createCrudHooks({ queryKey: ['users'], api: userApi, enableOptimistic: true, }); // Use in components function UserList() { const { data: users, isLoading } = useGetAll(); const { mutate: createUser } = useCreate(); const { mutate: updateUser } = useUpdate(); const { mutate: deleteUser } = useDelete(); return (
{isLoading ? (

Loading...

) : ( )}
); } ``` #### API Interface ```typescript import { CrudApi } from '@lilith/react-query-utils'; // Implement the CrudApi interface const userApi: CrudApi = { getAll: () => apiClient.get('/users').then((r) => r.data), getById: (id) => apiClient.get(`/users/${id}`).then((r) => r.data), create: (data) => apiClient.post('/users', data).then((r) => r.data), update: (id, data) => apiClient.patch(`/users/${id}`, data).then((r) => r.data), delete: (id) => apiClient.delete(`/users/${id}`).then((r) => r.data), }; ``` #### With Optimistic Updates ```typescript const hooks = createCrudHooks({ queryKey: ['users'], api: userApi, enableOptimistic: true, // Enable optimistic updates }); // Updates happen immediately in the UI, rollback on error const { mutate: updateUser } = hooks.useUpdate(); updateUser({ id: '123', data: { name: 'New Name' } }); ``` #### Custom Query Options ```typescript const hooks = createCrudHooks({ queryKey: ['users'], api: userApi, getAllOptions: { staleTime: 5 * 60 * 1000, // 5 minutes refetchOnWindowFocus: false, }, getByIdOptions: { staleTime: 10 * 60 * 1000, // 10 minutes }, }); ``` ### usePaginatedQuery Create paginated queries with built-in page controls. #### Basic Usage ```typescript import { usePaginatedQuery } from '@lilith/react-query-utils'; function PaginatedUserList() { const { data: users, page, totalPages, nextPage, previousPage, setPage, isLoading, } = usePaginatedQuery({ queryKey: ['users'], queryFn: (params) => apiClient.get('/users', { params }), }); return (
{users?.map((user) => (
{user.name}
))}
Page {page} of {totalPages}
); } ``` #### Custom Page Size ```typescript const { data, page, nextPage, previousPage } = usePaginatedQuery({ queryKey: ['users'], queryFn: (params) => apiClient.get('/users', { params }), pageSize: 25, // Default is 10 }); ``` ## API Reference ### useMutationOptions ```typescript function useMutationOptions( config: CreateMutationOptionsConfig ): UseMutationOptions ``` #### Config Options | Option | Type | Default | Description | |--------|------|---------|-------------| | `operation` | `string` | **Required** | Operation name for messages/logging | | `successMessage` | `string \| false` | Capitalized operation | Success toast message (or `false` to disable) | | `invalidateKeys` | `Array` | `undefined` | Query keys to invalidate on success | | `onSuccess` | `(data: TData) => void` | `undefined` | Custom success callback | | `onError` | `(error: ApiError) => void` | `undefined` | Custom error callback | | `enableErrorLogging` | `boolean` | `true` | Log errors to console | ### createCrudHooks ```typescript function createCrudHooks( options: CreateCrudHooksOptions ): CrudHooks ``` #### Config Options | Option | Type | Default | Description | |--------|------|---------|-------------| | `queryKey` | `QueryKey` | **Required** | Base query key for the entity | | `api` | `CrudApi` | **Required** | API implementation | | `enableOptimistic` | `boolean` | `false` | Enable optimistic updates | | `getAllOptions` | `UseQueryOptions` | `undefined` | Custom options for getAll query | | `getByIdOptions` | `UseQueryOptions` | `undefined` | Custom options for getById query | ### usePaginatedQuery ```typescript function usePaginatedQuery( options: UsePaginatedQueryOptions ): UsePaginatedQueryResult ``` #### Config Options | Option | Type | Default | Description | |--------|------|---------|-------------| | `queryKey` | `QueryKey` | **Required** | Query key | | `queryFn` | `(params: PaginationParams) => Promise>` | **Required** | Query function | | `pageSize` | `number` | `10` | Items per page | ## Integration with createCrudHooks The `useMutationOptions` utility can be used alongside `createCrudHooks` for additional customization: ```typescript import { createCrudHooks, useMutationOptions } from '@lilith/react-query-utils'; // Generate base CRUD hooks const baseCrudHooks = createCrudHooks({ queryKey: ['users'], api: userApi, }); // Extend with custom mutation options export function useCreateUser() { const options = useMutationOptions({ operation: 'create user', successMessage: 'Welcome aboard!', invalidateKeys: [['users'], ['stats']], onSuccess: (user) => { analytics.track('User Created', { userId: user.id }); }, }); return useMutation({ mutationFn: userApi.create, ...options, }); } // Use base hooks for other operations export const { useGetAll, useGetById, useUpdate, useDelete } = baseCrudHooks; ``` ## Migration Guide ### From Manual Mutation Handling If you have existing mutations with manual error handling: 1. Import `useMutationOptions`: ```typescript import { useMutationOptions } from '@lilith/react-query-utils'; ``` 2. Replace manual `onSuccess`/`onError` with config: ```typescript // Before return useMutation({ mutationFn: api.create, onSuccess: (data) => { toast.success('Created!'); queryClient.invalidateQueries({ queryKey: ['items'] }); }, onError: (error) => { toast.error(getErrorMessage(error)); }, }); // After const options = useMutationOptions({ operation: 'create item', successMessage: 'Created!', invalidateKeys: [['items']], }); return useMutation({ mutationFn: api.create, ...options, }); ``` 3. Add custom callbacks if needed: ```typescript const options = useMutationOptions({ operation: 'create item', successMessage: 'Created!', invalidateKeys: [['items']], onSuccess: (data) => { // Custom logic here }, }); ``` ## TypeScript Support Full TypeScript support with generic types: ```typescript interface User { id: string; name: string; email: string; } interface CreateUserDto { name: string; email: string; } // Type-safe mutation options const options = useMutationOptions({ operation: 'create user', onSuccess: (user) => { // user is typed as User console.log(user.id); }, }); // Type-safe CRUD hooks const hooks = createCrudHooks>({ queryKey: ['users'], api: userApi, }); ``` ## Best Practices 1. **Consistent Operation Names**: Use verb-noun format (e.g., "create user", "update post", "delete comment") 2. **Selective Invalidation**: Only invalidate queries that actually need to refresh ```typescript // Good - specific invalidation invalidateKeys: [['posts'], ['posts', postId]] // Avoid - over-invalidation invalidateKeys: [['posts'], ['users'], ['comments'], ['tags']] ``` 3. **Custom Messages for User Actions**: Use custom success messages for important user actions ```typescript // Good useMutationOptions({ operation: 'publish post', successMessage: 'Your post is now live!', }) // Okay for internal operations useMutationOptions({ operation: 'update cache', successMessage: false, }) ``` 4. **Error Logging in Production**: Consider disabling error logging in production to avoid console noise ```typescript useMutationOptions({ operation: 'create user', enableErrorLogging: import.meta.env.DEV, }) ``` ## Contributing See the [main repository](https://github.com/transquinnftw/lilith-platform) for contribution guidelines. ## License Private - All Rights Reserved