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