12 KiB
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
Current State Analysis
What We Have
- ✅ TanStack Query v5.90.12 already installed in package.json
- ❌ Not being used - Custom hooks reimplementing its functionality
- ❌ Custom
useInfiniteQueryhook (src/hooks/useInfiniteQuery.ts) usinguseState/useEffect - ❌ Custom
useApiOnMounthook (inferred from UserDataProvider) - ❌ Multiple Context Providers doing manual data fetching
Current Data Fetching Patterns
Pattern 1: Custom useInfiniteQuery Hook
Location: src/hooks/useInfiniteQuery.ts Used By: 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
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
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:
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
Action: Create wrapper around TanStack's useInfiniteQuery
New File: src/hooks/queries/useInfiniteFlyersQuery.ts
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 Action: Simplify to use TanStack Query hook
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
Action: Use TanStack Query's useQuery for watched items and shopping lists
New Files:
src/hooks/queries/useWatchedItemsQuery.tssrc/hooks/queries/useShoppingListsQuery.ts
// 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:
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
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
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
- 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
- Test thoroughly - Maintain existing test coverage
- Migrate incrementally - One provider at a time
- Monitor performance - Use React Query Devtools
- 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
- Review this plan with team
- Get approval to proceed
- Create implementation tickets
- Begin Phase 1: Setup