543 lines
13 KiB
Markdown
543 lines
13 KiB
Markdown
|
|
# @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 (
|
||
|
|
<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
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
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
|
||
|
|
|
||
|
|
```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 (
|
||
|
|
<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
|
||
|
|
|
||
|
|
```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<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
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
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
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
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:
|
||
|
|
|
||
|
|
```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<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
|
||
|
|
|
||
|
|
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
|