|
| 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