plans
2026-05-10
2026 05 10 Context Pattern Fix

Context Pattern Fix Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Fix two custom contexts violating the useContextSelector pattern — ReservationDraftContext and DashboardContext. Remove wrapper hooks, add proper default values, and update consumers to use useContextSelector directly.

Architecture: Each context follows the pattern: *.context.tsx exports only XContext with defaults; *.provider.tsx wraps with state. Components use useContextSelector(XContext, s => s.selector) directly — no wrapper hooks.

Tech Stack: use-context-selector@2.0.0, immer, React Context


File Inventory

FileChange
apps/frontend/contexts/reservation-draft-context.tsxRefactor: useContextSelector + default values + remove wrapper hook
apps/frontend/hooks/use-reservation-draft.tsDELETE — wrapper hook is banned
apps/frontend/components/admin/dashboard-context.tsxRefactor: useContextSelector + default values + remove wrapper hook
apps/frontend/components/admin/role-switcher.tsxUpdate: useContextSelector directly instead of useDashboard()
apps/frontend/components/admin/event-selector.tsxUpdate: useContextSelector directly instead of useDashboard()

Task 1: Fix ReservationDraftContext

Files:

  • Modify: apps/frontend/contexts/reservation-draft-context.tsx

  • Delete: apps/frontend/hooks/use-reservation-draft.ts

  • Step 1: Read the current reservation-draft-context.tsx

Review the full file to understand the context structure.

  • Step 2: Refactor to use useContextSelector with proper defaults

Replace the file content with the corrected pattern:

/**
 * ReservationDraftContext — provides reservation draft state via Convex
 * FIXED: 2026-05-10 - useContextSelector pattern, proper defaults
 */
 
"use client";
 
import {
  createContext,
  useEffect,
  useState,
  useCallback,
  type ReactNode,
} from "react";
import { useQuery, useMutation } from "convex/react";
import { useDebounceCallback } from "usehooks-ts";
import { api } from "@packages/backend/convex/_generated/api";
import { consola } from "consola";
import type { BookingStep } from "~/lib/constants/booking-steps";
 
const DEBOUNCE_MS = 500;
 
interface ReservationDraft {
  _id: string;
  eventId?: string;
  ticketType?: "DINNER_THEATRE" | "SHOW_ONLY";
  quantity?: number;
  addOns?: Array<{ addOnId: string; quantity: number }>;
  currentStep?: BookingStep;
  expiresAt?: number;
  [key: string]: unknown;
}
 
interface ReservationDraftContextValue {
  draft: ReservationDraft | null | undefined;
  isLoading: boolean;
  createError: string | null;
  updateField: (field: string, value: unknown) => void;
  setStep: (step: BookingStep) => void;
  complete: () => void;
}
 
const defaultContextValue: ReservationDraftContextValue = {
  draft: null,
  isLoading: true,
  createError: null,
  updateField: () => {},
  setStep: () => {},
  complete: () => {},
};
 
const ReservationDraftContext =
  createContext<ReservationDraftContextValue>(defaultContextValue);
 
export { ReservationDraftContext };
 
export function ReservationDraftProvider({
  children,
}: {
  children: ReactNode;
}) {
  const draft = useQuery(api.domains.reservations.bookingDrafts.getBySession);
  const createDraft = useMutation(
    api.domains.reservations.bookingDrafts.getOrCreate,
  );
  const updateDraft = useMutation(
    api.domains.reservations.bookingDrafts.updateDraft,
  );
  const deleteDraft = useMutation(
    api.domains.reservations.bookingDrafts.deleteDraft,
  );
 
  const [createError, setCreateError] = useState<string | null>(null);
 
  // Ensure draft exists on mount
  useEffect(() => {
    const initDraft = async () => {
      try {
        await createDraft({});
      } catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        setCreateError(message);
        consola.error("Failed to create reservation draft", { error: err });
      }
    };
    initDraft();
  }, [createDraft]);
 
  // Debounced field update
  const updateField = useDebounceCallback((field: string, value: unknown) => {
    updateDraft({ data: { [field]: value } });
  }, DEBOUNCE_MS);
 
  // Immediate update for step changes (no debounce)
  const setStep = useCallback(
    (step: BookingStep) => {
      updateDraft({ data: { currentStep: step } });
    },
    [updateDraft],
  );
 
  // Cleanup on completion
  const complete = useCallback(() => {
    deleteDraft({});
  }, [deleteDraft]);
 
  const value: ReservationDraftContextValue = {
    draft,
    isLoading: draft === undefined,
    createError,
    updateField,
    setStep,
    complete,
  };
 
  return (
    <ReservationDraftContext.Provider value={value}>
      {children}
    </ReservationDraftContext.Provider>
  );
}
  • Step 3: Delete the wrapper hook file

Delete: apps/frontend/hooks/use-reservation-draft.ts

  • Step 4: Run typecheck

Run: cd apps/frontend && pnpm tsc --noEmit 2>&1 | head -50 Expected: Type errors in files that still import useReservationDraftContext — those are the consumers to fix next.

  • Step 5: Commit
git add apps/frontend/contexts/reservation-draft-context.tsx apps/frontend/hooks/use-reservation-draft.ts
git commit -m "fix(context): useContextSelector pattern for ReservationDraftContext
 
- createContext with proper default values (not null)
- removed wrapper hook useReservationDraftContext
- delete use-reservation-draft.ts (wrapper hooks banned)"

Task 2: Update ReservationDraftContext consumers

Files:

  • Find all files importing useReservationDraftContext and update them to use useContextSelector directly.

  • Step 1: Find all consumers

Run: grep -rn "useReservationDraftContext" apps/frontend --include="*.tsx" --include="*.ts" Expected: List of files that need updating.

  • Step 2: For each consumer file, replace the import and usage

For each consumer, change:

// BEFORE
import { useReservationDraftContext } from "~/contexts/reservation-draft-context";
const { draft, isLoading } = useReservationDraftContext();
// AFTER
import { ReservationDraftContext } from "~/contexts/reservation-draft-context";
import { useContextSelector } from "use-context-selector";
 
const draft = useContextSelector(ReservationDraftContext, (s) => s.draft);
const isLoading = useContextSelector(
  ReservationDraftContext,
  (s) => s.isLoading,
);
  • Step 3: Run typecheck to verify

Run: cd apps/frontend && pnpm tsc --noEmit 2>&1 | head -50 Expected: No errors related to ReservationDraftContext.

  • Step 4: Commit
git add apps/frontend/[affected-consumer-files]
git commit -m "fix(context): useContextSelector directly in ReservationDraftContext consumers"

Task 3: Fix DashboardContext

Files:

  • Modify: apps/frontend/components/admin/dashboard-context.tsx

  • Step 1: Read and refactor dashboard-context.tsx

Replace the file content with the corrected pattern:

// ipsoc checked: 2026-05-08
// SoC: Centralized dashboard state - wraps nuqs for selectedEvent and role
// Premium UI: AURORA glass styling
 
"use client";
 
import { createContext, useMemo, useCallback, type ReactNode } from "react";
import { useQuery } from "convex/react";
import { useQueryState } from "nuqs";
import { api } from "@packages/backend/convex/_generated/api";
 
type Role = "ADMIN" | "STAFF";
 
interface DashboardContextValue {
  // Event selection (from nuqs)
  selectedEventId: string | null;
  setSelectedEventId: (id: string | null) => void;
  // Role (from nuqs)
  role: Role;
  setRole: (role: Role) => void;
  // Derived: full event object
  selectedEvent: {
    _id: string;
    showId: string;
    showTitle: string;
    date: string;
    time: string;
    status: string;
  } | null;
  // Loading states
  isEventsLoading: boolean;
}
 
const defaultContextValue: DashboardContextValue = {
  selectedEventId: null,
  setSelectedEventId: () => {},
  role: "ADMIN",
  setRole: () => {},
  selectedEvent: null,
  isEventsLoading: true,
};
 
const DashboardContext =
  createContext<DashboardContextValue>(defaultContextValue);
 
export { DashboardContext };
 
interface EventWithShow {
  _id: string;
  showId: string;
  showTitle: string;
  date: string;
  time: string;
  status: string;
}
 
function findNearestEvent(events: EventWithShow[]): string | null {
  if (events.length === 0) return null;
 
  const today = new Date().toISOString().split("T")[0];
  const currentTime = new Date().toTimeString().slice(0, 5);
 
  for (const event of events) {
    if (event.date > today) return event._id;
    if (event.date === today && event.time >= currentTime) return event._id;
  }
 
  return events[0]._id;
}
 
export function DashboardProvider({ children }: { children: ReactNode }) {
  const [selectedEventId, setSelectedEventId] = useQueryState("eventId");
  const [role, setRole] = useQueryState("role", {
    defaultValue: "ADMIN",
    serialize: (value) => value,
    parse: (value) => value as Role,
  });
 
  const events = useQuery(api.domains.shows.events.listForDashboard) as
    | EventWithShow[]
    | undefined;
 
  const isEventsLoading = events === undefined;
 
  // Auto-select nearest event on mount if none selected
  const effectiveSelectedEventId = useMemo(() => {
    if (selectedEventId) return selectedEventId;
    if (!events || events.length === 0) return null;
    return findNearestEvent(events);
  }, [selectedEventId, events]);
 
  // Update URL when auto-selected
  if (
    effectiveSelectedEventId &&
    effectiveSelectedEventId !== selectedEventId &&
    !selectedEventId
  ) {
    setTimeout(() => {
      setSelectedEventId(effectiveSelectedEventId);
    }, 0);
  }
 
  const selectedEvent = useMemo(
    () => events?.find((e) => e._id === effectiveSelectedEventId) ?? null,
    [events, effectiveSelectedEventId],
  );
 
  const handleSetSelectedEventId = useCallback(
    (id: string | null) => {
      setSelectedEventId(id || null);
    },
    [setSelectedEventId],
  );
 
  const handleSetRole = useCallback(
    (newRole: Role) => {
      setRole(newRole);
    },
    [setRole],
  );
 
  const value: DashboardContextValue = {
    selectedEventId: effectiveSelectedEventId,
    setSelectedEventId: handleSetSelectedEventId,
    role: role as Role,
    setRole: handleSetRole,
    selectedEvent,
    isEventsLoading,
  };
 
  return (
    <DashboardContext.Provider value={value}>
      {children}
    </DashboardContext.Provider>
  );
}
  • Step 2: Run typecheck

Run: cd apps/frontend && pnpm tsc --noEmit 2>&1 | head -50 Expected: Type errors in files importing useDashboard (role-switcher.tsx, event-selector.tsx).

  • Step 3: Commit
git add apps/frontend/components/admin/dashboard-context.tsx
git commit -m "fix(context): useContextSelector pattern for DashboardContext
 
- createContext with proper default values (not null)
- removed wrapper hook useDashboard
- export DashboardContext directly for useContextSelector"

Task 4: Update DashboardContext consumers

Files:

  • Modify: apps/frontend/components/admin/role-switcher.tsx

  • Modify: apps/frontend/components/admin/event-selector.tsx

  • Step 1: Update role-switcher.tsx

Change from:

import { useDashboard } from "~/components/admin/dashboard-context";
const { role, setRole } = useDashboard();

To:

import { DashboardContext } from "~/components/admin/dashboard-context";
import { useContextSelector } from "use-context-selector";
 
const role = useContextSelector(DashboardContext, (s) => s.role);
const setRole = useContextSelector(DashboardContext, (s) => s.setRole);
  • Step 2: Update event-selector.tsx

Change from:

import { useDashboard } from "~/components/admin/dashboard-context";
// ... inside component:
useDashboard(); // triggers auto-select logic

To:

import { DashboardContext } from "~/components/admin/dashboard-context";
import { useContextSelector } from "use-context-selector";
 
// ... inside component:
// Get selectedEventId to trigger auto-select effect
const selectedEventId = useContextSelector(
  DashboardContext,
  (s) => s.selectedEventId,
);
  • Step 3: Run typecheck to verify

Run: cd apps/frontend && pnpm tsc --noEmit 2>&1 | head -50 Expected: No errors.

  • Step 4: Commit
git add apps/frontend/components/admin/role-switcher.tsx apps/frontend/components/admin/event-selector.tsx
git commit -m "fix(context): useContextSelector directly in DashboardContext consumers"

Task 5: Final verification

  • Step 1: Run typecheck across entire frontend

Run: cd apps/frontend && pnpm tsc --noEmit 2>&1 Expected: Clean pass with no errors.

  • Step 2: Verify no useContext violations remain

Run: grep -rn "useContext(" apps/frontend/contexts apps/frontend/components/admin --include="*.tsx" | grep -v "useContextSelector" Expected: Only shadcn/ui primitives (CarouselContext, FormFieldContext, etc.) should remain.

  • Step 3: Verify no wrapper hooks remain

Run: grep -rn "export function use.*Context" apps/frontend --include="*.tsx" Expected: No results (wrapper hooks are banned).

  • Step 4: Final commit
git add -A
git commit -m "fix(context): complete context pattern migration
 
- ReservationDraftContext and DashboardContext now use useContextSelector
- All consumers use useContextSelector directly (no wrapper hooks)
- All contexts have proper default values (not null)"