Files
flyer-crawler.projectium.com/plans/adr-0005-implementation-plan.md
Torben Sorensen d356d9dfb6
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 43s
claude 1
2026-01-08 07:47:29 -08:00

427 lines
12 KiB
Markdown

# ADR-0005 Implementation Plan: Frontend State Management with TanStack Query
**Date**: 2026-01-08
**Status**: Ready for Implementation
**Related ADR**: [ADR-0005: Frontend State Management and Server Cache Strategy](../docs/adr/0005-frontend-state-management-and-server-cache-strategy.md)
## Current State Analysis
### What We Have
1.**TanStack Query v5.90.12 already installed** in package.json
2.**Not being used** - Custom hooks reimplementing its functionality
3.**Custom `useInfiniteQuery` hook** ([src/hooks/useInfiniteQuery.ts](../src/hooks/useInfiniteQuery.ts)) using `useState`/`useEffect`
4.**Custom `useApiOnMount` hook** (inferred from UserDataProvider)
5.**Multiple Context Providers** doing manual data fetching
### Current Data Fetching Patterns
#### Pattern 1: Custom useInfiniteQuery Hook
**Location**: [src/hooks/useInfiniteQuery.ts](../src/hooks/useInfiniteQuery.ts)
**Used By**: [src/providers/FlyersProvider.tsx](../src/providers/FlyersProvider.tsx)
**Problems**:
- Reimplements pagination logic that TanStack Query provides
- Manual loading state management
- Manual error handling
- No automatic caching
- No background refetching
- No request deduplication
#### Pattern 2: useApiOnMount Hook
**Location**: Unknown (needs investigation)
**Used By**: [src/providers/UserDataProvider.tsx](../src/providers/UserDataProvider.tsx)
**Problems**:
- Fetches data on mount only
- Manual loading/error state management
- No caching between unmount/remount
- Redundant state synchronization logic
## Implementation Strategy
### Phase 1: Setup TanStack Query Infrastructure (Day 1)
#### 1.1 Create QueryClient Configuration
**File**: `src/config/queryClient.ts`
```typescript
import { QueryClient } from '@tanstack/react-query';
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 30, // 30 minutes (formerly cacheTime)
retry: 1,
refetchOnWindowFocus: false,
refetchOnMount: true,
},
mutations: {
retry: 0,
},
},
});
```
#### 1.2 Wrap App with QueryClientProvider
**File**: `src/providers/AppProviders.tsx`
Add TanStack Query provider at the top level:
```typescript
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { queryClient } from '../config/queryClient';
export const AppProviders = ({ children }) => {
return (
<QueryClientProvider client={queryClient}>
{/* Existing providers */}
{children}
{/* Add devtools in development */}
{import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />}
</QueryClientProvider>
);
};
```
### Phase 2: Replace Custom Hooks with TanStack Query (Days 2-5)
#### 2.1 Replace useInfiniteQuery Hook
**Current**: [src/hooks/useInfiniteQuery.ts](../src/hooks/useInfiniteQuery.ts)
**Action**: Create wrapper around TanStack's `useInfiniteQuery`
**New File**: `src/hooks/queries/useInfiniteFlyersQuery.ts`
```typescript
import { useInfiniteQuery } from '@tanstack/react-query';
import * as apiClient from '../../services/apiClient';
export const useInfiniteFlyersQuery = () => {
return useInfiniteQuery({
queryKey: ['flyers'],
queryFn: async ({ pageParam }) => {
const response = await apiClient.fetchFlyers(pageParam);
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Failed to fetch flyers');
}
return response.json();
},
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
});
};
```
#### 2.2 Replace FlyersProvider
**Current**: [src/providers/FlyersProvider.tsx](../src/providers/FlyersProvider.tsx)
**Action**: Simplify to use TanStack Query hook
```typescript
import React, { ReactNode, useMemo } from 'react';
import { FlyersContext } from '../contexts/FlyersContext';
import { useInfiniteFlyersQuery } from '../hooks/queries/useInfiniteFlyersQuery';
export const FlyersProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const {
data,
isLoading,
error,
fetchNextPage,
hasNextPage,
isRefetching,
refetch,
} = useInfiniteFlyersQuery();
const flyers = useMemo(
() => data?.pages.flatMap((page) => page.items) ?? [],
[data]
);
const value = useMemo(
() => ({
flyers,
isLoadingFlyers: isLoading,
flyersError: error,
fetchNextFlyersPage: fetchNextPage,
hasNextFlyersPage: !!hasNextPage,
isRefetchingFlyers: isRefetching,
refetchFlyers: refetch,
}),
[flyers, isLoading, error, fetchNextPage, hasNextPage, isRefetching, refetch]
);
return <FlyersContext.Provider value={value}>{children}</FlyersContext.Provider>;
};
```
**Benefits**:
- ~100 lines of code removed
- Automatic caching
- Background refetching
- Request deduplication
- Optimistic updates support
#### 2.3 Replace UserDataProvider
**Current**: [src/providers/UserDataProvider.tsx](../src/providers/UserDataProvider.tsx)
**Action**: Use TanStack Query's `useQuery` for watched items and shopping lists
**New Files**:
- `src/hooks/queries/useWatchedItemsQuery.ts`
- `src/hooks/queries/useShoppingListsQuery.ts`
```typescript
// src/hooks/queries/useWatchedItemsQuery.ts
import { useQuery } from '@tanstack/react-query';
import * as apiClient from '../../services/apiClient';
export const useWatchedItemsQuery = (enabled: boolean) => {
return useQuery({
queryKey: ['watched-items'],
queryFn: async () => {
const response = await apiClient.fetchWatchedItems();
if (!response.ok) throw new Error('Failed to fetch watched items');
return response.json();
},
enabled,
});
};
// src/hooks/queries/useShoppingListsQuery.ts
import { useQuery } from '@tanstack/react-query';
import * as apiClient from '../../services/apiClient';
export const useShoppingListsQuery = (enabled: boolean) => {
return useQuery({
queryKey: ['shopping-lists'],
queryFn: async () => {
const response = await apiClient.fetchShoppingLists();
if (!response.ok) throw new Error('Failed to fetch shopping lists');
return response.json();
},
enabled,
});
};
```
**Updated Provider**:
```typescript
import React, { ReactNode, useMemo } from 'react';
import { UserDataContext } from '../contexts/UserDataContext';
import { useAuth } from '../hooks/useAuth';
import { useWatchedItemsQuery } from '../hooks/queries/useWatchedItemsQuery';
import { useShoppingListsQuery } from '../hooks/queries/useShoppingListsQuery';
export const UserDataProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const { userProfile } = useAuth();
const isEnabled = !!userProfile;
const { data: watchedItems = [], isLoading: isLoadingWatched, error: watchedError } =
useWatchedItemsQuery(isEnabled);
const { data: shoppingLists = [], isLoading: isLoadingLists, error: listsError } =
useShoppingListsQuery(isEnabled);
const value = useMemo(
() => ({
watchedItems,
shoppingLists,
isLoading: isEnabled && (isLoadingWatched || isLoadingLists),
error: watchedError?.message || listsError?.message || null,
}),
[watchedItems, shoppingLists, isEnabled, isLoadingWatched, isLoadingLists, watchedError, listsError]
);
return <UserDataContext.Provider value={value}>{children}</UserDataContext.Provider>;
};
```
**Benefits**:
- ~40 lines of code removed
- No manual state synchronization
- Automatic cache invalidation on user logout
- Background refetching
### Phase 3: Add Mutations for Data Modifications (Days 6-8)
#### 3.1 Create Mutation Hooks
**Example**: `src/hooks/mutations/useAddWatchedItemMutation.ts`
```typescript
import { useMutation, useQueryClient } from '@tanstack/react-query';
import * as apiClient from '../../services/apiClient';
import { notifySuccess, notifyError } from '../../services/notificationService';
export const useAddWatchedItemMutation = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: apiClient.addWatchedItem,
onSuccess: () => {
// Invalidate and refetch watched items
queryClient.invalidateQueries({ queryKey: ['watched-items'] });
notifySuccess('Item added to watched list');
},
onError: (error: Error) => {
notifyError(error.message || 'Failed to add item');
},
});
};
```
#### 3.2 Implement Optimistic Updates
**Example**: Optimistic shopping list update
```typescript
export const useUpdateShoppingListMutation = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: apiClient.updateShoppingList,
onMutate: async (newList) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['shopping-lists'] });
// Snapshot previous value
const previousLists = queryClient.getQueryData(['shopping-lists']);
// Optimistically update
queryClient.setQueryData(['shopping-lists'], (old) =>
old.map((list) => (list.id === newList.id ? newList : list))
);
return { previousLists };
},
onError: (err, newList, context) => {
// Rollback on error
queryClient.setQueryData(['shopping-lists'], context.previousLists);
notifyError('Failed to update shopping list');
},
onSettled: () => {
// Always refetch after error or success
queryClient.invalidateQueries({ queryKey: ['shopping-lists'] });
},
});
};
```
### Phase 4: Remove Old Custom Hooks (Day 9)
#### Files to Remove:
-`src/hooks/useInfiniteQuery.ts` (if not used elsewhere)
-`src/hooks/useApiOnMount.ts` (needs investigation)
#### Files to Update:
- Update any remaining usages in other components
### Phase 5: Testing & Documentation (Day 10)
#### 5.1 Update Tests
- Update provider tests to work with QueryClient
- Add tests for new query hooks
- Add tests for mutation hooks
#### 5.2 Update Documentation
- Mark ADR-0005 as **Accepted** and **Implemented**
- Add usage examples to documentation
- Update developer onboarding guide
## Migration Checklist
### Prerequisites
- [x] TanStack Query installed
- [ ] QueryClient configuration created
- [ ] App wrapped with QueryClientProvider
### Queries
- [ ] Flyers infinite query migrated
- [ ] Watched items query migrated
- [ ] Shopping lists query migrated
- [ ] Master items query migrated (if applicable)
- [ ] Active deals query migrated (if applicable)
### Mutations
- [ ] Add watched item mutation
- [ ] Remove watched item mutation
- [ ] Update shopping list mutation
- [ ] Add shopping list item mutation
- [ ] Remove shopping list item mutation
### Cleanup
- [ ] Remove custom useInfiniteQuery hook
- [ ] Remove custom useApiOnMount hook
- [ ] Update all tests
- [ ] Remove redundant state management code
### Documentation
- [ ] Update ADR-0005 status to "Accepted"
- [ ] Add usage guidelines to README
- [ ] Document query key conventions
- [ ] Document cache invalidation patterns
## Benefits Summary
### Code Reduction
- **Estimated**: ~300-500 lines of custom hook code removed
- **Result**: Simpler, more maintainable codebase
### Performance Improvements
- ✅ Automatic request deduplication
- ✅ Background data synchronization
- ✅ Smart cache invalidation
- ✅ Optimistic updates
- ✅ Automatic retry logic
### Developer Experience
- ✅ React Query Devtools for debugging
- ✅ Type-safe query hooks
- ✅ Standardized patterns across the app
- ✅ Less boilerplate code
### User Experience
- ✅ Faster perceived performance (cached data)
- ✅ Better offline experience
- ✅ Smoother UI interactions (optimistic updates)
- ✅ Automatic background updates
## Risk Assessment
### Low Risk
- TanStack Query is industry-standard
- Already installed in project
- Incremental migration possible
### Mitigation Strategies
1. **Test thoroughly** - Maintain existing test coverage
2. **Migrate incrementally** - One provider at a time
3. **Monitor performance** - Use React Query Devtools
4. **Rollback plan** - Keep old code until migration complete
## Timeline Estimate
**Total**: 10 working days (2 weeks)
- Day 1: Setup infrastructure
- Days 2-5: Migrate queries
- Days 6-8: Add mutations
- Day 9: Cleanup
- Day 10: Testing & documentation
## Next Steps
1. Review this plan with team
2. Get approval to proceed
3. Create implementation tickets
4. Begin Phase 1: Setup
## References
- [TanStack Query Documentation](https://tanstack.com/query/latest)
- [React Query Best Practices](https://tkdodo.eu/blog/practical-react-query)
- [ADR-0005 Original Document](../docs/adr/0005-frontend-state-management-and-server-cache-strategy.md)