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 |