Skip to content

Commit b0b0920

Browse files
committed
docs: add ADR-004 TanStack Query migration
- Document migration rationale and trade-offs - Add TanStack Query patterns to CLAUDE.md - Include testing patterns and key benefits Co-Authored-By: Claude
1 parent 61c03d9 commit b0b0920

File tree

2 files changed

+147
-0
lines changed

2 files changed

+147
-0
lines changed

CLAUDE.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,50 @@ Key implementation details:
7777
- Fetches in batches via `rvlimit=max` with continuation tokens (`rvcontinue`)
7878
- Client-side dump processing was considered and rejected due to prohibitive download times (ADR-003)
7979

80+
## TanStack Query Integration (ADR-004)
81+
82+
The application uses TanStack Query for page ID fetching with the following patterns:
83+
84+
### Query Configuration
85+
86+
- **QueryClientProvider** in @src/App.tsx with:
87+
- 5-minute `staleTime` for automatic caching
88+
- Single retry on failure
89+
- ReactQueryDevtools in development mode
90+
91+
### Page ID Fetching Pattern
92+
93+
@src/components/PageTitleInput.tsx:
94+
- Uses TanStack Pacer's `useDebouncedValue` (300ms) for input debouncing
95+
- Always renders ErrorBoundary > Suspense > PageIdFetcher (no conditional branching)
96+
97+
@src/components/PageIdFetcher.tsx:
98+
- Uses `useSuspenseQuery` with query key `['pageId', wikiUrl.href, pageTitle]`
99+
- Handles empty title inside `queryFn`: returns `null` for empty title (quick-resolving, no Suspense flash)
100+
- Passes `AbortSignal` from query to `fetchPageId` for request cancellation
101+
- Throws errors for ErrorBoundary to catch (no inline error handling)
102+
103+
### Key Benefits
104+
105+
- **Request cancellation**: In-flight requests automatically aborted on input change
106+
- **Automatic caching**: Same page title returns cached result (5min staleTime)
107+
- **Deduplication**: Multiple components requesting same data share one request
108+
- **Promise stability**: Query cache handles promise reference stability
109+
110+
### Testing Pattern
111+
112+
All components using queries must be wrapped in `QueryClientProvider`:
113+
114+
```tsx
115+
const renderWithQuery = (component: React.ReactElement) => {
116+
return render(
117+
<QueryClientProvider client={queryClient}>
118+
{component}
119+
</QueryClientProvider>
120+
);
121+
};
122+
```
123+
80124
## Testing Requirements (TDD Workflow)
81125

82126
Uses **Vitest** with **in-source testing** pattern. All source files MUST include tests:
@@ -130,6 +174,7 @@ Located in @docs/adr/:
130174
- **ADR-001**: Removed revision sampling in favor of linear streaming search for code simplicity
131175
- **ADR-002**: Enabled client-side caching with `maxage` parameter (60s for recent, 600s for historical)
132176
- **ADR-003**: Rejected client-side dump processing due to prohibitive download speeds
177+
- **ADR-004**: Migrated to TanStack Query for page ID fetching (replaced useMemo + use() pattern)
133178

134179
## Deployment
135180

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# ADR-004: Migration to TanStack Query
2+
3+
## Status
4+
5+
Accepted
6+
7+
## Context
8+
9+
The application initially used a `useMemo` + `use()` hook pattern for fetching page IDs in `PageTitleInput.tsx`. While functional, this pattern has several issues:
10+
11+
1. **Anti-pattern**: React 19 documentation explicitly warns against creating promises in Client Components
12+
2. **No request cancellation**: In-flight requests cannot be aborted when dependencies change
13+
3. **Manual memoization**: Requires careful dependency array management
14+
4. **Promise recreation**: Every dependency change creates a new promise
15+
5. **No caching**: Repeated searches for the same title refetch unnecessarily
16+
6. **Future risk**: React may tighten enforcement of this pattern in future versions
17+
18+
## Decision
19+
20+
Migrate from the `useMemo` + `use()` pattern to TanStack Query with the following architecture:
21+
22+
- Use `useSuspenseQuery` for data fetching (idiomatic React 19 pattern)
23+
- Use TanStack Pacer's `useDebouncedValue` for input debouncing (300ms)
24+
- Implement component composition pattern for conditional rendering (since `useSuspenseQuery` doesn't support `enabled` option)
25+
- Add `AbortSignal` support to `fetchPageId` for proper request cancellation
26+
- Set 5-minute `staleTime` for automatic caching
27+
28+
## Consequences
29+
30+
### Positive
31+
32+
- **Idiomatic React 19**: Follows official React guidance for data fetching
33+
- **Automatic caching**: Same title returns cached response (5min staleTime)
34+
- **Request cancellation**: AbortSignal propagated to fetch, cancels in-flight requests
35+
- **Promise stability**: Global query cache handles stability automatically
36+
- **Deduplication**: Multiple components requesting same data share one request
37+
- **DevTools**: TanStack Query DevTools available for debugging cache/queries
38+
- **Background refetch**: Configurable stale-while-revalidate behavior
39+
40+
### Negative
41+
42+
- **Bundle size increase**: ~6.3 KB gzipped (77.93 KB → 84.25 KB)
43+
- **Additional dependency**: Requires TanStack Query and Pacer packages
44+
- **Increased complexity**: Requires QueryProvider setup and understanding query concepts
45+
- **Learning curve**: Team needs to understand TanStack Query patterns
46+
47+
### Implementation Details
48+
49+
#### Component Composition Pattern
50+
51+
`PageTitleInput.tsx` always renders the same structure without conditional branching:
52+
53+
```tsx
54+
<ErrorBoundary fallback={<ErrorFallback />}>
55+
<Suspense fallback={<LoadingIndicator />}>
56+
<PageIdFetcher wikiUrl={wikiUrl} pageTitle={debouncedTitle} />
57+
</Suspense>
58+
</ErrorBoundary>
59+
```
60+
61+
`PageIdFetcher.tsx` handles empty title in `queryFn` to avoid Rules of Hooks violations:
62+
63+
```tsx
64+
const { data: pageId } = useSuspenseQuery({
65+
queryKey: ['pageId', wikiUrl.href, pageTitle],
66+
queryFn: ({ signal }) => pageTitle ? fetchPageId(wikiUrl, pageTitle, signal) : null,
67+
});
68+
```
69+
70+
When `pageTitle` is empty, the `queryFn` returns `null` immediately (no network request, no Suspense flash).
71+
72+
#### AbortSignal Propagation
73+
74+
```tsx
75+
export async function fetchPageId(
76+
baseUrl: URL,
77+
pageTitle: string,
78+
signal: AbortSignal | null = null,
79+
): Promise<number> {
80+
const response = await fetch(url, { signal });
81+
// ...
82+
}
83+
```
84+
85+
#### Query Configuration
86+
87+
```tsx
88+
const queryClient = new QueryClient({
89+
defaultOptions: {
90+
queries: {
91+
staleTime: 5 * 60 * 1000, // 5 minutes
92+
retry: 1,
93+
},
94+
},
95+
});
96+
```
97+
98+
## References
99+
100+
- [React 19 Documentation on use() hook](https://react.dev/reference/react/use)
101+
- [TanStack Query Documentation](https://tanstack.com/query/latest)
102+
- [TanStack Pacer Documentation](https://tanstack.com/pacer/latest)

0 commit comments

Comments
 (0)