Update shared packages to use local configs instead of central references: - Update .eslintrc.json files across all @packages/* - Update package.json dev dependencies - Update tsconfig.json extends paths - Update i18n common.json locale file This aligns with the config consolidation, allowing each package to manage its own configuration independently. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| src | ||
| .eslintignore | ||
| .eslintrc.json | ||
| CHANGELOG.md | ||
| EXAMPLES.md | ||
| package.json | ||
| QUICK_START.md | ||
| README.md | ||
| tsconfig.json | ||
| vitest.config.ts | ||
@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
pnpm add @lilith/react-query-utils
Usage
useMutationOptions
Create standardized mutation options with automatic error handling, toast notifications, and query invalidation.
Basic Usage
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
// 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
// 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
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
// 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):
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):
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
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 (
<div>
{isLoading ? (
<p>Loading...</p>
) : (
<ul>
{users?.map((user) => (
<li key={user.id}>
{user.name}
<button onClick={() => deleteUser(user.id)}>Delete</button>
</li>
))}
</ul>
)}
<button onClick={() => createUser({ name: 'New User' })}>
Create User
</button>
</div>
);
}
API Interface
import { CrudApi } from '@lilith/react-query-utils';
// Implement the CrudApi interface
const userApi: CrudApi<User, CreateUserDto, UpdateUserDto> = {
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
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
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
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 (
<div>
{users?.map((user) => (
<div key={user.id}>{user.name}</div>
))}
<div>
<button onClick={previousPage} disabled={page === 1}>
Previous
</button>
<span>
Page {page} of {totalPages}
</span>
<button onClick={nextPage} disabled={page >= totalPages}>
Next
</button>
</div>
</div>
);
}
Custom Page Size
const { data, page, nextPage, previousPage } = usePaginatedQuery({
queryKey: ['users'],
queryFn: (params) => apiClient.get('/users', { params }),
pageSize: 25, // Default is 10
});
API Reference
useMutationOptions
function useMutationOptions<TData = unknown, TVariables = unknown>(
config: CreateMutationOptionsConfig
): UseMutationOptions<TData, ApiError, TVariables>
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<string | string[]> |
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
function createCrudHooks<TEntity, TCreateDto, TUpdateDto>(
options: CreateCrudHooksOptions<TEntity, TCreateDto, TUpdateDto>
): CrudHooks<TEntity, TCreateDto, TUpdateDto>
Config Options
| Option | Type | Default | Description |
|---|---|---|---|
queryKey |
QueryKey |
Required | Base query key for the entity |
api |
CrudApi<TEntity, TCreateDto, TUpdateDto> |
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
function usePaginatedQuery<TData>(
options: UsePaginatedQueryOptions<TData>
): UsePaginatedQueryResult<TData>
Config Options
| Option | Type | Default | Description |
|---|---|---|---|
queryKey |
QueryKey |
Required | Query key |
queryFn |
(params: PaginationParams) => Promise<PaginatedResponse<TData>> |
Required | Query function |
pageSize |
number |
10 |
Items per page |
Integration with createCrudHooks
The useMutationOptions utility can be used alongside createCrudHooks for additional customization:
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:
-
Import
useMutationOptions:import { useMutationOptions } from '@lilith/react-query-utils'; -
Replace manual
onSuccess/onErrorwith config:// 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, }); -
Add custom callbacks if needed:
const options = useMutationOptions({ operation: 'create item', successMessage: 'Created!', invalidateKeys: [['items']], onSuccess: (data) => { // Custom logic here }, });
TypeScript Support
Full TypeScript support with generic types:
interface User {
id: string;
name: string;
email: string;
}
interface CreateUserDto {
name: string;
email: string;
}
// Type-safe mutation options
const options = useMutationOptions<User, CreateUserDto>({
operation: 'create user',
onSuccess: (user) => {
// user is typed as User
console.log(user.id);
},
});
// Type-safe CRUD hooks
const hooks = createCrudHooks<User, CreateUserDto, Partial<User>>({
queryKey: ['users'],
api: userApi,
});
Best Practices
-
Consistent Operation Names: Use verb-noun format (e.g., "create user", "update post", "delete comment")
-
Selective Invalidation: Only invalidate queries that actually need to refresh
// Good - specific invalidation invalidateKeys: [['posts'], ['posts', postId]] // Avoid - over-invalidation invalidateKeys: [['posts'], ['users'], ['comments'], ['tags']] -
Custom Messages for User Actions: Use custom success messages for important user actions
// Good useMutationOptions({ operation: 'publish post', successMessage: 'Your post is now live!', }) // Okay for internal operations useMutationOptions({ operation: 'update cache', successMessage: false, }) -
Error Logging in Production: Consider disabling error logging in production to avoid console noise
useMutationOptions({ operation: 'create user', enableErrorLogging: import.meta.env.DEV, })
Contributing
See the main repository for contribution guidelines.
License
Private - All Rights Reserved