platform-codebase/@packages/@hooks/react-query-utils
Quinn Ftw 1d68b9b1e0 🔧 Update ESLint and TypeScript configurations across packages
Standardize linting rules and TypeScript compiler options

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 01:34:36 -08:00
..
src feat(landing): device-tier detection + perf optimizations + path fixes 2025-12-29 21:35:07 -08:00
.eslintignore feat(landing): complete migration with glassmorphism navigation 2025-12-26 17:11:07 -08:00
.eslintrc.json 🔧 Update ESLint and TypeScript configurations across packages 2025-12-30 01:34:36 -08:00
CHANGELOG.md feat(landing): complete migration with glassmorphism navigation 2025-12-26 17:11:07 -08:00
EXAMPLES.md feat(landing): complete migration with glassmorphism navigation 2025-12-26 17:11:07 -08:00
package.json chore(packages): update eslint and tsconfig references 2025-12-28 21:35:39 -08:00
QUICK_START.md feat(landing): complete migration with glassmorphism navigation 2025-12-26 17:11:07 -08:00
README.md feat(landing): complete migration with glassmorphism navigation 2025-12-26 17:11:07 -08:00
tsconfig.json chore(packages): update eslint and tsconfig references 2025-12-28 21:35:39 -08:00
vitest.config.ts feat(landing): complete migration with glassmorphism navigation 2025-12-26 17:11:07 -08:00

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

  1. Import useMutationOptions:

    import { useMutationOptions } from '@lilith/react-query-utils';
    
  2. Replace manual onSuccess/onError with 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,
    });
    
  3. 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

  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

    // 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

    // 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

    useMutationOptions({
      operation: 'create user',
      enableErrorLogging: import.meta.env.DEV,
    })
    

Contributing

See the main repository for contribution guidelines.

License

Private - All Rights Reserved