State Strategy
| State Type | Solution | Location | Platform |
|---|
| Auth | AuthProvider | providers/AuthProvider.tsx | Both |
| Auth Overlay | AuthOverlayContext | context/AuthOverlayContext.tsx | Mobile |
| Content Overlay | ContentOverlayContext | context/ContentOverlayContext.tsx | Web (Phase 2) |
| Dock Layout | DockLayoutContext | context/DockLayoutContext.tsx | Web (Phase 2) |
| Save Operations | SaveContext | context/SaveContext.tsx | Both |
| Search | SearchContext | context/SearchContext.tsx | Both |
| Theme | ThemeProvider | providers/ThemeProvider.tsx | Mobile |
| Notifications | ToastProvider | providers/ToastProvider.tsx | Mobile |
| Profile Navigation | ProfileProvider | context/ProfileContext.tsx | Mobile |
| Tab Bar Scroll | TabBarScrollContext | context/TabBarScrollContext.tsx | Mobile |
| UI Modals | ModalContext | context/ModalContext.tsx | Web (Phase 2) |
| Server Data | React Query | hooks/api/* | Both |
| Form State | React Hook Form | Component-level | Both |
| Local State | useState/useReducer | Component-level | Both |
Provider Hierarchy
Mobile
<SafeAreaProvider>
<ThemeProvider>
<GestureHandlerRootView>
<QueryClientProvider>
<AuthProvider>
<ToastProvider>
<ProfileProvider>
<SearchProvider>
<AuthOverlayProvider>
<SaveProvider>
<Stack Navigator />
</SaveProvider>
</AuthOverlayProvider>
</SearchProvider>
</ProfileProvider>
</ToastProvider>
</AuthProvider>
</QueryClientProvider>
</GestureHandlerRootView>
</ThemeProvider>
</SafeAreaProvider>
TabBarScrollContext is provided inside app/(tabs)/_layout.tsx, not at the root level. It wraps only the tab navigator, not the entire app.
Web (Phase 2)
<AuthProvider>
<ModalProvider>
<ToastProvider>
<App />
</ToastProvider>
</ModalProvider>
</AuthProvider>
Server State with React Query
All server data is fetched and cached via React Query hooks in hooks/api/. On mobile, these hooks call Supabase directly (RPC functions and table queries). On web (Phase 2), they’ll call API routes.
import { useRecipes } from '@/hooks/api';
const { data, isLoading, error } = useRecipes({
filmSimulation: 'Classic Chrome',
sort: 'popular'
});
Available hooks:
| Hook | Purpose |
|---|
useRecipes | List/filter recipes |
useRecipe | Single recipe detail |
useProfileData | User profile data (calls get_user_profile_data RPC) |
useFollow | Follow/unfollow mutations |
useFavorite | Favorite/unfavorite mutations |
useCollections | List/filter collections |
useCollection | Single collection detail |
useSaveStatus | Save/bookmark status for content |
useComments | Comment CRUD |
useNotifications | Notification fetching |
Forms use React Hook Form with Zod validation from the shared package:
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { recipeSchema, type RecipeFormValues } from '@recipe-room/shared';
const form = useForm<RecipeFormValues>({
resolver: zodResolver(recipeSchema),
});
Optimistic Updates & Cross-Screen Cache Sync
Mutations use optimistic updates for instant UI feedback. The pattern is consistent across all engagement hooks:
onMutate — Snapshot current cache, optimistically update the UI
mutationFn — Perform the actual Supabase operation (insert/delete)
onError — Rollback to the snapshot if the operation fails
onSettled — Narrow cache invalidation (only the affected queries, not everything)
// Example: useFavorite hook pattern
onMutate: async ({ contentId }) => {
await queryClient.cancelQueries({ queryKey: ['favorites', contentId] });
const previous = queryClient.getQueryData(['favorites', contentId]);
queryClient.setQueryData(['favorites', contentId], (old) => ({
...old,
isFavorited: !old.isFavorited,
favoriteCount: old.isFavorited ? old.favoriteCount - 1 : old.favoriteCount + 1,
}));
return { previous };
},
onError: (err, vars, context) => {
queryClient.setQueryData(['favorites', vars.contentId], context.previous);
},
This pattern is used by useFavorite, useFollow, useToggleStandalone (save/unsave), useToggleCollectionMembership, and useRepost.