Problem
Currently each entity (employees, contracts, caces, etc.) has its own set of custom hooks:
// Multiple files per entity, each 60-80 lines of boilerplate
useEmployees.ts // list + detail
useCreateEmployee.ts // mutation with optimistic update
useUpdateEmployee.ts // mutation with optimistic update
useDeleteEmployee.ts // mutation with optimistic update
This creates:
- Massive code duplication (~60-80 lines × 4 hooks × N entities)
- Inconsistent patterns - some entities invalidate related queries, others don't
- Maintenance burden - every new field requires updating multiple files
- No type inference - lots of
any and manual typing
Proposed Solution
1. Generic hooks working directly with ORPC procedures
// Instead of entity-specific hooks:
const { data } = useEmployee(123); // useEmployees.ts
const create = useCreateEmployee(); // useCreateEmployee.ts
// Use generic hooks with ORPC procedures:
const { data } = useQuery(api.database.employees.get, { id: 123 });
const create = useMutation(api.database.employees.create);
// On page:
const { data: employees } = useQuery(api.database.employees.list);
create.mutate({ firstName: "John", lastName: "Doe" });
2. Cache Registry - Type-safe cache configuration per entity
A type encapsulating both query keys and mutation invalidation keys:
// Per-entity cache configuration
const employeeCache = {
queries: {
list: ['employees', 'list'] as const,
detail: (id: number) => ['employees', 'detail', id] as const,
},
mutations: {
create: {
invalidate: ['employees', 'list', 'contracts', 'alerts'] as const,
optimisticFn: (input) => { /* transform for optimistic update */ },
},
update: {
invalidate: ['employees', 'list', 'employees.detail'] as const,
},
delete: {
invalidate: ['employees', 'list', 'trash.deletedEmployees'] as const,
},
},
} satisfies CacheRegistry;
3. Hook integration
// useQuery with cache registry
const { data } = useQuery(api.database.employees.list, {
cache: employeeCache.queries.list,
});
// useMutation with cache registry - invalidate and optimistic update
const create = useMutation(api.database.employees.create, {
cache: employeeCache.mutations.create,
});
4. Hook Implementation Design
// Generic useQuery
export function useQuery<TProcedure extends ORPCProcedure>(
procedure: TProcedure,
input: InferInput<TProcedure>,
options?: QueryOptions
) {
const orpcReady = useORPCReady();
return useQuery({
queryKey: buildQueryKey(procedure, input),
queryFn: () => procedure(input),
enabled: orpcReady,
...options,
});
}
// Generic useMutation with optimistic updates
export function useMutation<TProcedure extends ORPCProcedure>(
procedure: TProcedure,
options?: MutationOptions<TProcedure>
) {
return useMutation({
mutationFn: (input) => procedure(input),
onMutate: async (input) => {
await cancelQueries(buildQueryKey(procedure));
const snapshot = snapshotQueries(buildQueryKey(procedure));
// optimistic update using cache.optimisticFn...
return { snapshot };
},
onError: (err, input, context) => {
// rollback from snapshot...
toast({ variant: "destructive", ... });
},
onSuccess: () => {
// invalidate all keys from cache.invalidate...
queryClient.invalidateQueries({ queryKey: buildQueryKey(procedure) });
},
...options,
});
}
5. Query Key Generation
Query keys derived from ORPC procedure paths:
// api.database.employees.list → ['employees', 'list']
// api.database.employees.get → ['employees', 'get', input.id]
// api.database.caces.create → ['caces', 'create']
Tasks
Benefits
- DRY - Single implementation for all entities
- Consistent - Same pattern everywhere, no ad-hoc variations
- Type-safe - Types inferred from ORPC procedures
- Maintainable - Adding a field only requires updating the page, not 4 hook files
- Discoverable - API mirrors server structure (api.database.employees.*)
- Centralized - Cache invalidation rules co-located per entity
Impact
- Removes ~20 hook files of boilerplate code
- Makes pages simpler and more readable
- Aligns client API with server router structure
- Clear separation between query cache keys and mutation invalidation keys
Problem
Currently each entity (employees, contracts, caces, etc.) has its own set of custom hooks:
This creates:
anyand manual typingProposed Solution
1. Generic hooks working directly with ORPC procedures
2. Cache Registry - Type-safe cache configuration per entity
A type encapsulating both query keys and mutation invalidation keys:
3. Hook integration
4. Hook Implementation Design
5. Query Key Generation
Query keys derived from ORPC procedure paths:
Tasks
useQueryanduseMutationhooksCacheRegistrytype for entity cache configurationQueryCacheKeysandMutationCacheKeysinterfacesbuildQueryKeyutility from ORPC procedure pathsCacheRegistryper entity (employees, contracts, caces, etc.)optimisticFnBenefits
Impact