Skip to content

Template: Add a new query hook

Status: Reference

Task

Add a new query hook for <read operation> (e.g. "fetch club members", "fetch session matches").

Pre-implementation checks

  1. Grep for similar query: grep -rn "useFind<X>\|useGet<X>\|use<X>List" packages/app/src/presentation/hooks/queries/. If a similar query exists, justify creating a new one OR extend the existing.
  2. Confirm port + adapter exist: repository method must already exist in packages/app/src/ports/ and be implemented in adapters.
  3. Pick freshness policy (will become explicit @freshness in Phase 14f):
    • realtime — sessions, RSVPs, matches; pushed via Realtime channels
    • frequent — leaderboards, open pickups, club elo
    • standard — club list, session list, member list (default)
    • stable — profile, preferences, venues
  4. Identify cache normalization opportunity: does the response shape match an entity that's stored in another query's cache? If yes, plan to use writeX / writeXBackfill from @/presentation/cache.

Files in scope

  • New: packages/app/src/presentation/hooks/queries/use-<noun>.ts
  • Possibly: packages/app/src/index.ts (barrel)
  • Possibly: packages/app/src/query-keys.ts (add the key factory if missing)

Files OUT of scope

  • Adapter changes — separate prior commit
  • UI changes — separate later commit

Implementation pattern

ts
import { useEffect } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { <repo> } from '@/registry';
import { <Keys>, STALE_TIME } from '@/query-keys';
import { write<Entity> } from '@/presentation/cache';  // optional — only if normalization applies
import type { <Entity> } from '@/domain';
import type { UseQueryResult } from '@tanstack/react-query';

/**
 * @freshness <realtime|frequent|standard|stable>
 * @refetchOnFocus <true|false>
 * @realtime <channel-key|null>
 * @offline <read-from-cache|n/a>
 *
 * Fetch description.
 */
export function use<Noun>(<args>): UseQueryResult<<Entity> | null> {
  const queryClient = useQueryClient();
  const result = useQuery({
    queryKey: <Keys>.<key-factory>(<args>),
    queryFn: () => <repo>.<method>(<args>),
    enabled: <args>.length > 0,
    staleTime: STALE_TIME.<bucket>,
  });

  // Cache normalization: keep parent lists in sync (optional, when applicable)
  useEffect(() => {
    if (result.data) write<Entity>(queryClient, result.data);
  }, [result.data, queryClient]);

  return result;
}

Hard constraints to follow (from AGENTS.md)

  • DATA-8: cross-query entity writes via writeX / writeXBackfill — never manual setQueryData
  • Reviewer checklist: CHK-DATA-2 (useMemo not useQuery for pure CPU), CHK-DATA-3 (don't fire the same key from two levels) — see docs/best-practices/review-checklist.md

Post-implementation verification

bash
git status
yarn workspace @twomore/app typecheck
yarn workspace @twomore/app jest --runInBand src/config/__tests__/architecture-archunit.test.ts

What to report

  • File created
  • Freshness policy chosen + reason
  • Whether cache normalization wired (and which entity)
  • Typecheck + arch test results

Markdown remains the source of truth. Run yarn docs:check before handoff.