# Data Fetching and Forms in Consuming Apps

Longer patterns for apps built on `@strongtie/design-system`, pulled out of the base skill so it
stays scannable. Two things to keep straight while reading:

- **The forms patterns are grounded in this repo.** `react-hook-form`, `zod`, and
  `@hookform/resolvers` are real dependencies here (`apps/docs/package.json`, and `react-hook-form`
  is also a dependency of `packages/design-system`). Use them as the default form stack.
- **The data-fetching patterns are recommendations for consuming apps, not facts about this repo.**
  Neither TanStack Query (`@tanstack/react-query`) nor Zustand is installed anywhere in this
  monorepo. (Do not over-correct into "no `@tanstack/*` here" — `@tanstack/react-table`, a table
  primitive unrelated to Query, *is* a dependency of `packages/design-system`.) The Query patterns
  below are the shape to reach for *if your app adopts server-state caching*; they are not something
  the design system ships or requires.

---

## State management, for an app that adopts a server-cache library

When an app grows past local component state, the decision that matters is *where each piece of
state lives*, and the failure mode is putting server data in the wrong place. A workable default
split for a consuming app:

1. **Server data (from an API)** → a server-cache library such as TanStack Query. It owns fetching,
   caching, and invalidation; do not shadow it in `useState`.
2. **Component-local UI state** (open/closed, hover, a draft value) → `useState`.
3. **Session-level shared state** (auth, tenant, theme) → React Context. Context is for values that
   change rarely and are read widely.
4. **Complex cross-component client state** → a dedicated store (Zustand or similar) *only if*
   Context re-render cost is actually hurting you. Reach for it last, not first.

The anti-patterns worth naming: never copy an API response into `useState` (you now have two sources
of truth that disagree on the next refetch), never mirror a query cache in local state, and never
create a Context for state a single component owns.

## TanStack Query patterns (if your app uses it)

### Query-key factory

Centralize keys so invalidation targets are consistent and typo-proof:

```typescript
// hooks/query-keys.ts
export const queryKeys = {
  projects: {
    all: ["projects"] as const,
    lists: () => [...queryKeys.projects.all, "list"] as const,
    list: (filters: ProjectFilters) =>
      [...queryKeys.projects.lists(), filters] as const,
    details: () => [...queryKeys.projects.all, "detail"] as const,
    detail: (id: string) => [...queryKeys.projects.details(), id] as const,
  },
}
```

### Custom query hooks

Wrap `useQuery` in a named hook rather than calling it inline in a component, so the key, the
fetcher, and the `enabled` gating live in one place and can be reused:

```typescript
export function useProjects() {
  const { selectedTenant } = useTenantContext()
  const isAuthReady = useAuthReady()

  return useQuery({
    queryKey: queryKeys.projects.list({ tenantId: selectedTenant?.id }),
    queryFn: projectsApi.getAll,
    enabled: isAuthReady,
  })
}
```

### Optimistic updates

Cancel in-flight queries, snapshot the previous cache, apply the optimistic value, and roll back on
error:

```typescript
export function useCreateProject() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: projectsApi.create,
    onMutate: async (newProject) => {
      await queryClient.cancelQueries({ queryKey: queryKeys.projects.lists() })
      const previous = queryClient.getQueryData(queryKeys.projects.list({}))
      queryClient.setQueryData(queryKeys.projects.list({}), (old) => [
        ...(old ?? []),
        { ...newProject, id: `temp-${Date.now()}` },
      ])
      return { previous }
    },
    onError: (_err, _vars, context) => {
      queryClient.setQueryData(queryKeys.projects.list({}), context?.previous)
    },
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: queryKeys.projects.lists() })
    },
  })
}
```

## Forms: React Hook Form + Zod

Reach for React Hook Form and Zod once a form has validation or more than a handful of fields; below
that, plain `useState` is less machinery for the same result.

| Condition                     | Solution              |
| ----------------------------- | --------------------- |
| 1–3 fields AND no validation  | `useState`            |
| 4+ fields OR validation rules | React Hook Form + Zod |

Define the schema once and let it drive both runtime validation and the inferred form type, so the
shape you validate and the shape you type can never disagree:

```typescript
const schema = z.object({
  email: z.string().email(),
  name: z.string().min(2),
})

const form = useForm<z.infer<typeof schema>>({
  resolver: zodResolver(schema),
})
```

## Error handling

Route each error class to the handler built for it, rather than surfacing everything the same way:

| Error type            | Handler                   |
| --------------------- | ------------------------- |
| React component crash | Error Boundary            |
| API error (expected)  | Query error state + toast |
| Validation error      | Form field errors         |
| Network error         | Toast notification        |

APIs return error codes, not user-facing prose. Map codes to friendly messages in one place (for
example `lib/error-messages.ts`) and never render a raw API error string to a user — it leaks
internals and rarely tells them what to do next.
