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
| File | Change |
|---|---|
apps/frontend/contexts/reservation-draft-context.tsx | Refactor: useContextSelector + default values + remove wrapper hook |
apps/frontend/hooks/use-reservation-draft.ts | DELETE — wrapper hook is banned |
apps/frontend/components/admin/dashboard-context.tsx | Refactor: useContextSelector + default values + remove wrapper hook |
apps/frontend/components/admin/role-switcher.tsx | Update: useContextSelector directly instead of useDashboard() |
apps/frontend/components/admin/event-selector.tsx | Update: 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
useReservationDraftContextand update them to useuseContextSelectordirectly. -
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 logicTo:
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)"