platform-codebase/@packages/@hooks/react-query-utils/src/use-mutation-options.tsx

146 lines
3.9 KiB
TypeScript
Executable file

import { useMemo } from 'react'
import { ApiError, getErrorMessage } from '@lilith/api-client'
import { UseMutationOptions, useQueryClient } from '@tanstack/react-query'
import toast from 'react-hot-toast'
/**
* Configuration for creating standardized mutation options
*/
export interface CreateMutationOptionsConfig<TData = unknown> {
/**
* Name of the operation being performed (e.g., "create user", "update post")
* Used in success/error messages and logging
*/
operation: string;
/**
* Custom success message to display
* - string: Display custom message
* - false: Don't display any success message
* - undefined: Display default message based on operation name
*/
successMessage?: string | false;
/**
* Query keys to invalidate on successful mutation
* Can be a single key or array of keys
* @example ['users'] or [['users'], ['posts', '123']]
*/
invalidateKeys?: Array<string | string[]>;
/**
* Custom success callback
* Called after query invalidation
*/
onSuccess?: (data: TData) => void;
/**
* Custom error callback
* Called after error toast display
*/
onError?: (error: ApiError) => void;
/**
* Enable error logging to console
* @default true
*/
enableErrorLogging?: boolean;
}
/**
* Create standardized mutation options with automatic error handling and query invalidation
*
* Provides:
* - Automatic success/error toast notifications
* - Query cache invalidation on success
* - Structured error logging
* - Consistent error message extraction
*
* @example
* ```typescript
* function useCreateUser() {
* const options = useMutationOptions({
* operation: 'create user',
* successMessage: 'User created successfully!',
* invalidateKeys: [['users']],
* onSuccess: (user) => {
* console.log('Created:', user);
* },
* });
*
* return useMutation({
* mutationFn: (data) => apiClient.post('/users', data),
* ...options,
* });
* }
* ```
*
* @example
* ```typescript
* // Disable success message
* const options = useMutationOptions({
* operation: 'update settings',
* successMessage: false,
* invalidateKeys: [['settings']],
* });
* ```
*
* @example
* ```typescript
* // Invalidate multiple query keys
* const options = useMutationOptions({
* operation: 'delete post',
* invalidateKeys: [['posts'], ['posts', postId], ['user', userId, 'posts']],
* });
* ```
*/
export function useMutationOptions<TData = unknown, TVariables = unknown>(
config: CreateMutationOptionsConfig<TData>
): UseMutationOptions<TData, ApiError, TVariables> {
const queryClient = useQueryClient()
return useMemo(
() => ({
onSuccess: (data: TData) => {
// Display success toast (unless explicitly disabled)
if (config.successMessage !== false) {
const message = config.successMessage || `${capitalize(config.operation)} successful`
toast.success(message)
}
// Invalidate specified query keys
if (config.invalidateKeys) {
config.invalidateKeys.forEach((key) => {
queryClient.invalidateQueries({ queryKey: Array.isArray(key) ? key : [key] })
})
}
// Call custom success callback
config.onSuccess?.(data)
},
onError: (error: ApiError) => {
// Extract and display error message
const message = getErrorMessage(error)
toast.error(message || `Failed to ${config.operation}`)
// Log error to console (unless explicitly disabled)
if (config.enableErrorLogging !== false) {
console.error(`[${config.operation}] Error:`, error)
}
// Call custom error callback
config.onError?.(error)
},
}),
[config, queryClient]
)
}
/**
* Capitalize the first letter of a string
*/
function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1)
}