Skip to main content

State Strategy

State TypeSolutionLocationPlatform
AuthAuthProviderproviders/AuthProvider.tsxBoth
Auth OverlayAuthOverlayContextcontext/AuthOverlayContext.tsxMobile
Content OverlayContentOverlayContextcontext/ContentOverlayContext.tsxWeb (Phase 2)
Dock LayoutDockLayoutContextcontext/DockLayoutContext.tsxWeb (Phase 2)
Save OperationsSaveContextcontext/SaveContext.tsxBoth
SearchSearchContextcontext/SearchContext.tsxBoth
ThemeThemeProviderproviders/ThemeProvider.tsxMobile
NotificationsToastProviderproviders/ToastProvider.tsxMobile
Profile NavigationProfileProvidercontext/ProfileContext.tsxMobile
Tab Bar ScrollTabBarScrollContextcontext/TabBarScrollContext.tsxMobile
UI ModalsModalContextcontext/ModalContext.tsxWeb (Phase 2)
Server DataReact Queryhooks/api/*Both
Form StateReact Hook FormComponent-levelBoth
Local StateuseState/useReducerComponent-levelBoth

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:
HookPurpose
useRecipesList/filter recipes
useRecipeSingle recipe detail
useProfileDataUser profile data (calls get_user_profile_data RPC)
useFollowFollow/unfollow mutations
useFavoriteFavorite/unfavorite mutations
useCollectionsList/filter collections
useCollectionSingle collection detail
useSaveStatusSave/bookmark status for content
useCommentsComment CRUD
useNotificationsNotification fetching

Form State

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:
  1. onMutate — Snapshot current cache, optimistically update the UI
  2. mutationFn — Perform the actual Supabase operation (insert/delete)
  3. onError — Rollback to the snapshot if the operation fails
  4. 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.