Template: Add a new mutation hook
Status: Reference
Task
Add a new mutation hook for <domain operation> (e.g. "update club name", "create dues record").
Pre-implementation checks
- Grep for similar mutation:
grep -rn "useUpdate<X>\|useCreate<X>\|use<X>Status" packages/app/src/presentation/hooks/mutations/. If a similar mutation exists, justify creating a new one OR extend the existing. - Confirm port + adapter exist: the underlying repository method must already exist in
packages/app/src/ports/and be implemented inpackages/app/src/adapters/supabase/. - Identify cache keys affected: what query keys does this mutation invalidate or patch? List them.
- Decide: optimistic or not? If the target row is already in cache, this should be optimistic (
applyOptimisticPatches). If it creates a row with a server-assigned ID, it shouldn't be optimistic.
Files in scope
- New:
packages/app/src/presentation/hooks/mutations/use-<verb>-<noun>.ts - Possibly:
packages/app/src/index.ts(if hook needs to be exported via barrel) - Possibly:
packages/app/src/registry.ts(only if a new repository method is needed)
Files OUT of scope
- Adapter changes — those go in a separate, prior commit
- UI changes — those consume this hook in a separate, later commit
Implementation pattern
If non-optimistic (factory):
ts
import { <repo> } from '@/registry';
import { <Keys> } from '@/query-keys';
import { t } from '@/config';
import { createMutationHook } from '@/presentation/hooks/create-mutation-hook';
import type { <Entity>, <Input> } from '@/domain';
/**
* @optimistic no — server assigns ID
* @offline queue-and-replay (Phase 14d)
* @invalidates <Keys>.all
*/
export const use<Verb><Noun> = createMutationHook<<Input>, <Entity>>({
mutationFn: (input) => <repo>.<method>(input),
successMessage: () => t().<scope>.toast.<key>,
errorMessage: () => t().errors.<key>,
invalidateKeys: () => [<Keys>.all],
});If optimistic (manual):
ts
import { useMutation, useQueryClient, type UseMutationResult } from '@tanstack/react-query';
import { <repo> } from '@/registry';
import { <Keys> } from '@/query-keys';
import { t } from '@/config';
import { showToast } from '@/presentation/stores/toast.store';
import { haptic } from '@/presentation/utils/haptics';
import {
applyOptimisticPatches,
rollbackOptimisticPatches,
type OptimisticContext,
} from '@/presentation/hooks/utils/optimistic';
import type { <Entity>, <Input> } from '@/domain';
/**
* @optimistic yes — patches <Keys>.<list> via prefix match
* @offline queue-and-replay (Phase 14d)
*/
export function use<Verb><Noun>(): UseMutationResult<<Entity>, Error, <Input>, OptimisticContext> {
const queryClient = useQueryClient();
return useMutation<<Entity>, Error, <Input>, OptimisticContext>({
mutationFn: (input) => <repo>.<method>(input),
onMutate: async (input) =>
applyOptimisticPatches({
queryClient,
patches: [
{
queryKey: <Keys>.<list>,
exact: true,
updater: (old) => /* immutable patch */,
},
],
}),
onSuccess: () => {
haptic.success();
showToast(t().<scope>.toast.<key>);
},
onError: (err, _vars, context) => {
rollbackOptimisticPatches(queryClient, context);
haptic.error();
showToast(err.message || t().errors.<key>, 'error');
},
onSettled: () => {
void queryClient.invalidateQueries({ queryKey: <Keys>.<list> });
},
});
}Hard constraints to follow (from AGENTS.md)
- DATA-2: factory or manual
useMutationwithapplyOptimisticPatches. Always carry amutationKey - DATA-4: errors via
captureCriticalError, notSentry.captureExceptiondirectly - Reviewer checklist:
CHK-DATA-1(optimistic when target row is cached) — seedocs/best-practices/review-checklist.md
Post-implementation verification
bash
# 1. Verify changes persisted
git status # should show new file
git diff --name-only HEAD # should include the new file
# 2. Type-check
yarn workspace @twomore/app typecheck # exit 0
# 3. Tests
yarn workspace @twomore/app jest --runInBand <hook-test-path> # if you wrote a test
# 4. Architecture contracts
yarn workspace @twomore/app jest --runInBand src/config/__tests__/architecture-archunit.test.tsWhat to report
- Files created/modified (with
git statusoutput) - Optimistic strategy chosen (and why)
- Cache keys affected
- Typecheck + arch test results
- Any rules in AGENTS.md you decided to flag for review