Skip to content

Commit feed0d0

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 0e3b701 commit feed0d0

File tree

2 files changed

+170
-0
lines changed

2 files changed

+170
-0
lines changed

CLAUDE.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,54 @@ 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+
- 1-hour `staleTime` for automatic caching (page IDs are immutable)
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 QueryErrorResetBoundary > ErrorBoundary > Suspense > PageIdFetcher (no conditional branching)
96+
- Uses `react-error-boundary` package with `resetKeys={[debouncedTitle]}` for automatic error recovery on title change
97+
- Discriminates 404 errors (page not found) from other errors using `isNotFoundError()` helper
98+
- Shows retry button with inline link styling for non-404 errors
99+
- Renders hidden `<input name="pageId">` in error fallback to prevent form submission
100+
101+
@src/components/PageIdFetcher.tsx:
102+
- Uses `useSuspenseQuery` with query key `['pageId', wikiUrl.href, pageTitle]`
103+
- Handles empty title inside `queryFn`: returns `null` for empty title (quick-resolving, no Suspense flash)
104+
- Passes `AbortSignal` from query to `fetchPageId` for request cancellation
105+
- Throws errors for ErrorBoundary to catch (no inline error handling)
106+
107+
### Key Benefits
108+
109+
- **Request cancellation**: In-flight requests automatically aborted on input change
110+
- **Automatic caching**: Same page title returns cached result (1h staleTime, page IDs are immutable)
111+
- **Deduplication**: Multiple components requesting same data share one request
112+
- **Promise stability**: Query cache handles promise reference stability
113+
114+
### Testing Pattern
115+
116+
All components using queries must be wrapped in `QueryClientProvider`:
117+
118+
```tsx
119+
const renderWithQuery = (component: React.ReactElement) => {
120+
return render(
121+
<QueryClientProvider client={queryClient}>
122+
{component}
123+
</QueryClientProvider>
124+
);
125+
};
126+
```
127+
80128
## Testing Requirements (TDD Workflow)
81129

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

134183
## Deployment
135184

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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 1-hour `staleTime` for automatic caching (page IDs are immutable)
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 (1h staleTime, page IDs are immutable)
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**: ~13.91 KB gzipped (70.74 KB → 84.65 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+
<QueryErrorResetBoundary>
55+
{({ reset }) => (
56+
<ErrorBoundary
57+
resetKeys={[debouncedTitle]}
58+
onReset={reset}
59+
fallbackRender={({ error, resetErrorBoundary }) => (
60+
// Error UI with retry button and hidden pageId input
61+
)}
62+
>
63+
<Suspense fallback={<LoadingIndicator />}>
64+
<PageIdFetcher wikiUrl={wikiUrl} pageTitle={debouncedTitle} />
65+
</Suspense>
66+
</ErrorBoundary>
67+
)}
68+
</QueryErrorResetBoundary>
69+
```
70+
71+
The component uses `react-error-boundary` package for enhanced error handling:
72+
- **`QueryErrorResetBoundary`**: Coordinates error boundary resets with TanStack Query cache
73+
- **`resetKeys={[debouncedTitle]}`**: Automatically resets error boundary when title changes
74+
- **`onReset={reset}`**: Synchronizes with TanStack Query's error state
75+
- **404 discrimination**: Uses `isNotFoundError()` helper to distinguish 404s from other errors
76+
- **Retry button**: Displayed only for non-404 errors with inline link styling
77+
- **Hidden input**: Renders `<input name="pageId">` in error fallback to prevent form submission with invalid page ID
78+
79+
`PageIdFetcher.tsx` handles empty title in `queryFn` to avoid Rules of Hooks violations:
80+
81+
```tsx
82+
const { data: pageId } = useSuspenseQuery({
83+
queryKey: ['pageId', wikiUrl.href, pageTitle],
84+
queryFn: ({ signal }) => pageTitle ? fetchPageId(wikiUrl, pageTitle, signal) : null,
85+
});
86+
```
87+
88+
When `pageTitle` is empty, the `queryFn` returns `null` immediately (no network request, no Suspense flash).
89+
90+
#### AbortSignal Propagation
91+
92+
```tsx
93+
export async function fetchPageId(
94+
baseUrl: URL,
95+
pageTitle: string,
96+
signal: AbortSignal | null = null,
97+
): Promise<number> {
98+
const response = await fetch(url, { signal });
99+
// ...
100+
}
101+
```
102+
103+
#### Query Configuration
104+
105+
```tsx
106+
const queryClient = new QueryClient({
107+
defaultOptions: {
108+
queries: {
109+
staleTime: 1 * 60 * 60 * 1000, // 1 hour (page IDs are immutable)
110+
retry: 1,
111+
},
112+
},
113+
});
114+
```
115+
116+
## References
117+
118+
- [React 19 Documentation on use() hook](https://react.dev/reference/react/use)
119+
- [TanStack Query Documentation](https://tanstack.com/query/latest)
120+
- [TanStack Pacer Documentation](https://tanstack.com/pacer/latest)
121+
- [react-error-boundary Documentation](https://github.com/bvaughn/react-error-boundary)

0 commit comments

Comments
 (0)