platform-codebase/@packages/@hooks/react-query-utils/README.md

543 lines
13 KiB
Markdown
Raw Normal View History

# @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