@@ -6,6 +6,7 @@ The management console for ThinkWatch, built with React 19, TypeScript, and Vite
66
77- ** React 19** with TypeScript
88- ** TanStack Router** for file-based routing
9+ - ** TanStack Query** for server state — see [ Data fetching] ( #data-fetching )
910- ** shadcn/ui** (Radix UI + Tailwind CSS 4) for components
1011- ** react-i18next** for internationalization (English + Chinese)
1112- ** Vitest** + React Testing Library for testing
@@ -23,28 +24,36 @@ pnpm exec tsc --noEmit # Type check
2324
2425## Data fetching
2526
26- Loads are hand-rolled: a ` useCallback ` that fetches and sets state, plus a
27- ` useEffect ` that calls it. There is no data-fetching layer in this app.
28-
29- That shape trips ` react-hooks/set-state-in-effect ` , and the sites that it
30- flags carry a one-line suppression saying which of two things is going on:
31-
32- - ** The rule is wrong.** ` useEffect(() => { load(); }, [load]) ` where ` load `
33- is async and its first statement is the ` await ` . Every setState inside
34- runs in the continuation — never synchronously with the effect, never a
35- cascading render. The rule's cross-function analysis does not model
36- ` await ` .
37- - ** The rule is right and the fix is architectural.** The loader's first
38- statement flips a spinner. Hoisting that flag out to render-time silences
39- the rule, but it splits "start a load" across two places and leaves every
40- other caller of the loader responsible for remembering half of it.
41-
42- ** Both go away with a data-fetching layer** (TanStack Query or equivalent),
43- which owns the loading flag and the cache and removes the effect entirely.
44- That is a deliberate piece of work, not something to fold into a lint pass.
45- Until then, do not add new suppressions of this rule without one of the two
46- reasons above — every other finding it reports is a real one, and the rest
47- of the codebase is clean of them.
27+ Server state goes through [ TanStack Query] ( https://tanstack.com/query ) . A
28+ component asks for what it renders with ` useQuery ` , and the query layer owns
29+ the rest: the loading flag, the cache, cancelling a request nobody is waiting
30+ for, and keeping a late response from landing under filters that have since
31+ changed. No load runs in an effect.
32+
33+ - ** Keys follow the endpoint.** ` GET /api/admin/teams/{id}/members ` is keyed
34+ ` ['admin', 'teams', id, 'members'] ` , with query-string parameters in a
35+ trailing object. A key names everything its ` queryFn ` reads —
36+ ` @tanstack/query/exhaustive-deps ` enforces that — and invalidating a
37+ prefix such as ` ['admin', 'teams'] ` reaches every query under it. Pass
38+ ` exact: true ` when a prefix would reach further than the write did.
39+ - ** Writes invalidate.** After a mutation, `await
40+ queryClient.invalidateQueries({ queryKey })` for what the screen shows.
41+ Awaiting keeps orderings like "close the dialog once the list is fresh".
42+ Screens that aren't open need nothing: every successful write through
43+ ` api() ` drops the cached queries no screen is using (` onSuccessfulWrite ` ,
44+ wired up in ` main.tsx ` ), so none of them reopens onto pre-write data.
45+ - ** Pass the signal.** ` queryFn: ({ signal }) => api(path, { signal }) ` lets
46+ the layer cancel a request once no component needs its answer.
47+ - ** Derive, don't copy.** Read ` query.data ` where it is rendered. When a
48+ screen needs local state that follows what was loaded — a form draft, a
49+ selection — adjust it during render, the way ` useResetOnChange ` does.
50+
51+ ` src/lib/query-client.ts ` builds the client and says why its defaults differ
52+ from the library's. Tests render through ` renderWithQueryClient ` from
53+ ` src/test/render.tsx ` , which gives every call an empty cache.
54+
55+ Do not add new suppressions of ` react-hooks/set-state-in-effect ` — every
56+ finding it reports is a real one, and the codebase is clean of them.
4857
4958## Project Structure
5059
5867│ └── use-mobile.ts # Responsive breakpoint detection
5968├── lib/
6069│ ├── api.ts # HTTP client with HMAC signing & auto token refresh
70+ │ ├── query-client.ts # TanStack Query client & its defaults
6171│ └── utils.ts # Utility functions
6272├── i18n/
6373│ ├── en.json # English translations
8898│ ├── settings.tsx # Dynamic system settings (7 tabs)
8999│ └── log-forwarders.tsx # Log forwarding configuration
90100├── test/
101+ │ ├── render.tsx # render() inside a fresh query cache
91102│ └── setup.ts # Test setup (jest-dom + i18n)
92103└── router.tsx # Route definitions & setup redirect logic
93104```
0 commit comments