Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,54 @@ Key implementation details:
- Fetches in batches via `rvlimit=max` with continuation tokens (`rvcontinue`)
- Client-side dump processing was considered and rejected due to prohibitive download times (ADR-003)

## TanStack Query Integration (ADR-004)

The application uses TanStack Query for page ID fetching with the following patterns:

### Query Configuration

- **QueryClientProvider** in @src/App.tsx with:
- 1-hour `staleTime` for automatic caching (page IDs are immutable)
- Single retry on failure
- ReactQueryDevtools in development mode

### Page ID Fetching Pattern

@src/components/PageTitleInput.tsx:
- Uses TanStack Pacer's `useDebouncedValue` (300ms) for input debouncing
- Always renders QueryErrorResetBoundary > ErrorBoundary > Suspense > PageIdFetcher (no conditional branching)
- Uses `react-error-boundary` package with `resetKeys={[debouncedTitle]}` for automatic error recovery on title change
- Discriminates 404 errors (page not found) from other errors using `isNotFoundError()` helper
- Shows retry button with inline link styling for non-404 errors
- Renders hidden `<input name="pageId">` in error fallback to prevent form submission

@src/components/PageIdFetcher.tsx:
- Uses `useSuspenseQuery` with query key `['pageId', wikiUrl.href, pageTitle]`
- Handles empty title inside `queryFn`: returns `null` for empty title (quick-resolving, no Suspense flash)
- Passes `AbortSignal` from query to `fetchPageId` for request cancellation
- Throws errors for ErrorBoundary to catch (no inline error handling)

### Key Benefits

- **Request cancellation**: In-flight requests automatically aborted on input change
- **Automatic caching**: Same page title returns cached result (1h staleTime, page IDs are immutable)
- **Deduplication**: Multiple components requesting same data share one request
- **Promise stability**: Query cache handles promise reference stability

### Testing Pattern

All components using queries must be wrapped in `QueryClientProvider`:

```tsx
const renderWithQuery = (component: React.ReactElement) => {
return render(
<QueryClientProvider client={queryClient}>
{component}
</QueryClientProvider>
);
};
```

## Testing Requirements (TDD Workflow)

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

## Deployment

Expand Down
121 changes: 121 additions & 0 deletions docs/adr/004-tanstack-query-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# ADR-004: Migration to TanStack Query

## Status

Accepted

## Context

The application initially used a `useMemo` + `use()` hook pattern for fetching page IDs in `PageTitleInput.tsx`. While functional, this pattern has several issues:

1. **Anti-pattern**: React 19 documentation explicitly warns against creating promises in Client Components
2. **No request cancellation**: In-flight requests cannot be aborted when dependencies change
3. **Manual memoization**: Requires careful dependency array management
4. **Promise recreation**: Every dependency change creates a new promise
5. **No caching**: Repeated searches for the same title refetch unnecessarily
6. **Future risk**: React may tighten enforcement of this pattern in future versions

## Decision

Migrate from the `useMemo` + `use()` pattern to TanStack Query with the following architecture:

- Use `useSuspenseQuery` for data fetching (idiomatic React 19 pattern)
- Use TanStack Pacer's `useDebouncedValue` for input debouncing (300ms)
- Implement component composition pattern for conditional rendering (since `useSuspenseQuery` doesn't support `enabled` option)
- Add `AbortSignal` support to `fetchPageId` for proper request cancellation
- Set 1-hour `staleTime` for automatic caching (page IDs are immutable)

## Consequences

### Positive

- **Idiomatic React 19**: Follows official React guidance for data fetching
- **Automatic caching**: Same title returns cached response (1h staleTime, page IDs are immutable)
- **Request cancellation**: AbortSignal propagated to fetch, cancels in-flight requests
- **Promise stability**: Global query cache handles stability automatically
- **Deduplication**: Multiple components requesting same data share one request
- **DevTools**: TanStack Query DevTools available for debugging cache/queries
- **Background refetch**: Configurable stale-while-revalidate behavior

### Negative

- **Bundle size increase**: ~13.91 KB gzipped (70.74 KB → 84.65 KB)
- **Additional dependency**: Requires TanStack Query and Pacer packages
- **Increased complexity**: Requires QueryProvider setup and understanding query concepts
- **Learning curve**: Team needs to understand TanStack Query patterns

### Implementation Details

#### Component Composition Pattern

`PageTitleInput.tsx` always renders the same structure without conditional branching:

```tsx
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
resetKeys={[debouncedTitle]}
onReset={reset}
fallbackRender={({ error, resetErrorBoundary }) => (
// Error UI with retry button and hidden pageId input
)}
>
<Suspense fallback={<LoadingIndicator />}>
<PageIdFetcher wikiUrl={wikiUrl} pageTitle={debouncedTitle} />
</Suspense>
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
```

The component uses `react-error-boundary` package for enhanced error handling:
- **`QueryErrorResetBoundary`**: Coordinates error boundary resets with TanStack Query cache
- **`resetKeys={[debouncedTitle]}`**: Automatically resets error boundary when title changes
- **`onReset={reset}`**: Synchronizes with TanStack Query's error state
- **404 discrimination**: Uses `isNotFoundError()` helper to distinguish 404s from other errors
- **Retry button**: Displayed only for non-404 errors with inline link styling
- **Hidden input**: Renders `<input name="pageId">` in error fallback to prevent form submission with invalid page ID

`PageIdFetcher.tsx` handles empty title in `queryFn` to avoid Rules of Hooks violations:

```tsx
const { data: pageId } = useSuspenseQuery({
queryKey: ['pageId', wikiUrl.href, pageTitle],
queryFn: ({ signal }) => pageTitle ? fetchPageId(wikiUrl, pageTitle, signal) : null,
});
```

When `pageTitle` is empty, the `queryFn` returns `null` immediately (no network request, no Suspense flash).

#### AbortSignal Propagation

```tsx
export async function fetchPageId(
baseUrl: URL,
pageTitle: string,
signal: AbortSignal | null = null,
): Promise<number> {
const response = await fetch(url, { signal });
// ...
}
```

#### Query Configuration

```tsx
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1 * 60 * 60 * 1000, // 1 hour (page IDs are immutable)
retry: 1,
},
},
});
```

## References

- [React 19 Documentation on use() hook](https://react.dev/reference/react/use)
- [TanStack Query Documentation](https://tanstack.com/query/latest)
- [TanStack Pacer Documentation](https://tanstack.com/pacer/latest)
- [react-error-boundary Documentation](https://github.com/bvaughn/react-error-boundary)
156 changes: 155 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@
},
"dependencies": {
"@streamparser/json-whatwg": "^0.0.22",
"@tanstack/react-pacer": "^0.19.4",
"@tanstack/react-query": "^5.90.20",
"@tanstack/react-query-devtools": "^5.91.3",
"react": "^19.2.4",
"react-dom": "^19.2.4"
"react-dom": "^19.2.4",
"react-error-boundary": "^6.1.0"
},
"devDependencies": {
"@eslint/js": "^9.39.2",
Expand Down
Loading