Skip to content

Form & List Hook Patterns

The webapp standardizes CRUD operations and paginated list management into reusable hook shapes used across 20+ entity types.


Form Hook Pattern

Each entity has a form hook that handles create, update, and delete mutations.

Standard Return Shape

{
  existingItem?: EntityType;       // Fetched when editing (undefined in create mode)
  isLoadingItem: boolean;          // Loading state for the edit-mode fetch
  loadError: Error | null;         // Fetch error (if any)
  createItem: (data) => Promise<Entity>;
  updateItem: ({ id, data }) => Promise<Entity>;
  deleteItem: (id) => Promise<void>;
  isCreating: boolean;
  isUpdating: boolean;
  isDeleting: boolean;
}

Behavior: - All async methods throw on error — the caller decides whether to catch or bubble to an error boundary - Toast notifications fire automatically on success and error - React Query cache is invalidated on every successful mutation

Example

import { useSubjectForm } from "@/hooks/useSubjectForm";

// Create mode
const form = useSubjectForm({ mode: "create" });
await form.createSubject(data);

// Edit mode — also fetches the existing subject
const form = useSubjectForm({ mode: "edit", subjectId: id });
// form.existingSubject — available once loaded
await form.updateSubject({ id, data });
await form.deleteSubject(id);

List Hook Pattern

Each entity has a list hook that handles paginated, searchable data fetching.

Standard Return Shape

{
  items: Entity[];
  isLoading: boolean;
  totalCount: number;
  currentPage: number;
  pageSize: number;
  totalPages: number;
  searchQuery: string;
  setSearchQuery: (query: string) => void;
  setCurrentPage: (page: number) => void;
  setPageSize?: (size: number) => void;
  refetch: () => void;
  recentlyCopied?: Entity | null;  // Present on some entities
}

Behavior: - Search is debounced (300 ms) before hitting the API - Pagination resets to page 1 when the search query changes - recentlyCopied is used to pin a recently-created item at the top of the table

Example

import { useSubjectsList } from "@/hooks/useSubjectsList";

const {
  subjects,
  isLoading,
  totalCount,
  currentPage,
  searchQuery,
  setSearchQuery,
  setCurrentPage,
} = useSubjectsList();

Infinite Query Pattern

For large datasets that load on demand, some hooks use TanStack useInfiniteQuery:

{
  items: Entity[];
  totalCount: number;
  isLoading: boolean;
  hasNextPage: boolean;
  fetchNextPage: () => void;
}

Examples: - useApprovePostProdData — auto-fetches all pages for bulk operations - useInfiniteSubjects — exposes manual fetchNextPage for virtual lists


Entity Catalog

All hook pairs available in the codebase:

Entity Form Hook List Hook
Subject useSubjectForm useSubjectsList
Set Design useSetDesignForm useSetDesignsList
Shooting Guideline useShootingGuidelineForm useShootingGuidelinesList
Shot Type useShotTypeForm useShotTypesList
Resolution Preset useResolutionPresetForm useResolutionPresetsList
Background Preset useBackgroundPresetForm useBackgroundPresetsList
Pose Preset usePosePresetForm usePosePresetsList
Stylist useStylistForm useStylistsList
Integration useIntegrationForm useIntegrationsList
Generation Model useGenerationModelForm useGenerationModelsList

useScopedPresets(orgId, ownerId?)

Fetches all preset types in a single hook. Use this in forms that need multiple preset selectors to avoid parallel waterfall requests:

import { useScopedPresets } from "@/hooks/useScopedPresets";

const {
  resolutionPresets,
  backgroundPresets,
  shotTypes,
  subjects,
  posePresets,
  setDesigns,
  isLoadingPresets,
} = useScopedPresets(orgId, ownerId);

The ownerId scopes presets to a specific owner (e.g., a production request). Pass null or omit it for organization-wide presets.


useDebounce(value, delay)

Generic debounce for search inputs. Used internally by all list hooks:

import { useDebounce } from "@/hooks/useDebounce";

const debouncedQuery = useDebounce(searchQuery, 300);

When to Add a New Hook Pair

Add a form + list hook pair when an entity needs create/edit/delete forms anywhere in the app. Keeping mutation logic and cache invalidation co-located in the hook prevents duplication across pages.

Checklist when creating a new pair: 1. Form hook: expose the standard return shape; add toast notifications; invalidate the list query key on success 2. List hook: use useDebounce for the search query; reset page to 1 on search change; pass keepPreviousData: true to avoid flickering


Source Files

File Contents
src/hooks/useSubjectForm.ts Subject form hook (reference implementation)
src/hooks/useSubjectsList.ts Subject list hook (reference implementation)
src/hooks/useSetDesignForm.ts Set Design form hook
src/hooks/useSetDesignsList.ts Set Design list hook
src/hooks/useScopedPresets.ts Multi-preset fetcher
src/hooks/useDebounce.ts Generic debounce hook