| name | frontend |
| description | React frontend guidance for diplicity-react โ components, state management, forms, data loading, routing, mock data, screenshots, and testing. Use when implementing or reviewing React features, refactoring frontend code, or taking PR screenshots. |
| allowed-tools | Task, Bash, Read, Glob, Grep, Write, Edit, TodoWrite |
Frontend Development (/packages/web/)
Architecture
- State: TanStack Query (React Query) for server state โ no Redux
- Routing: React Router
- UI: shadcn/ui + Tailwind CSS
- Testing: Vitest + Testing Library
- Build: Vite + TypeScript
Files in src/api/generated/ are auto-generated by orval. Never edit manually โ run docker compose up codegen to regenerate.
When to Invoke This Skill
- Writing new React components or features
- Refactoring existing React code
- Implementing forms and validation
- Adding data loading with TanStack Query or RTK Query
- Working with React Router
- Writing or updating React tests
- Reviewing React code changes
- Taking PR screenshots
General Workflow
-
Before starting: Review existing components in src/components/ for patterns to follow. Understand the current tech stack (React 19, TypeScript, Vite).
-
Component creation: (See ./react-components.md)
- Check existing components in
src/components/ first
- Use shadcn/ui components (transitioning from Material UI)
- Keep components focused and under 200 lines
- Remember we're using React 19 (no
forwardRef, <Context> as provider)
-
Data fetching: (See ./react-data-loading.md and ./react-zod.md)
- Always use
useXxxSuspense hooks for reads โ never non-suspense variants
- Use
useXxxMutation hooks with mutateAsync for writes (not mutate)
- No Redux โ do not add it for any purpose
-
Forms: (See ./react-hook-form.md and ./react-zod.md)
- Use React Hook Form + Zod for all new forms
-
Navigation:
- Use React Router hooks:
useNavigate, useParams, useSearchParams, useLocation
- Use
Link for declarative navigation
- Store shareable/bookmarkable state in URL
- Use
useRequiredParams for typed route params guaranteed by route structure โ eliminates runtime null checks
-
Testing: (See ./react-tests.md)
- Write tests in
.test.tsx files alongside components
- Mock APIs with MSW (Mock Service Worker)
- Use Testing Library patterns
- Run tests with
npm run test
-
Code review:
- Run
npm run lint to check issues
- Run
npm run build or npx tsc -b --noEmit to ensure TypeScript compilation
- Fix any lint violations properly โ never disable rules
Data & State Management
React Query rules
- Always use
useXxxSuspense hooks for reads โ never non-suspense variants
- Use
useXxxMutation hooks with mutateAsync for writes (not mutate)
- No Redux โ do not add it for any purpose
Mutations in useEffect dependencies
Never include mutation objects in useEffect dependency arrays โ the object gets a new reference on every state change (idle โ pending โ success/error), causing infinite loops.
const mut = useCreateMutation();
useEffect(() => { mut.mutateAsync(data); }, [condition, mut]);
const mut = useCreateMutation();
useEffect(() => { mut.mutateAsync(data); }, [condition]);
State hierarchy
- Backend โ source of truth for all domain data
- URL โ navigation state, tabs, filters (
useSearchParams, useParams)
- Local state โ pure UI concerns only (e.g.
isEditingName)
If state can be derived from the backend or URL, it must be.
Review check: domain data in useState? navigation state in useState? useEffect syncing state? All should be derived instead.
Suspense guarantees
Use Suspense so components can assume data is already loaded โ no loading checks inside components. Fetch directly from URL params โ do not pass entity data through Context:
const { gameId } = useRequiredParams<{ gameId: string }>();
const { data: game } = useGameRetrieveSuspense(gameId);
React Query deduplicates requests, so multiple components fetching the same ID share one request.
Component Patterns
Organisation
- Screens:
src/screens/ (or feature subdirectories like screens/GameDetail/)
- Shared components:
src/components/
- Keep it flat
Suspense wrapper pattern
Every screen that fetches data needs this structure:
const MyScreen: React.FC = () => {
const { data } = useDataSuspense();
return <div>...</div>;
};
const MyScreenSuspense: React.FC = () => (
<ScreenContainer>
<ScreenHeader title="My Screen" />
<QueryErrorBoundary>
<Suspense fallback={<MyScreenSkeleton />}>
<MyScreen />
</Suspense>
</QueryErrorBoundary>
</ScreenContainer>
);
export { MyScreenSuspense as MyScreen };
QueryErrorBoundary must wrap Suspense (not be inside it) โ otherwise query errors crash the whole page instead of showing a "Try Again" UI in the content area.
Review check: inner component uses useXxxSuspense? outer has ScreenContainer + ScreenHeader + QueryErrorBoundary + Suspense? fallback is a skeleton that mirrors the screen's content and matches its dimensions (not an empty div or a centered spinner, and the skeleton-to-content swap must not shift layout โ see the UX skill's loading-states guidance)? wrapper exported under the screen name?
Inline over extract
Inline sub-components and utility functions that are only used in one place. Only extract to separate files if genuinely shared across multiple screens. See CreateGame.tsx as a canonical example.
Prop types
Always use explicit interface definitions for props. Infer types for local return types.
interface GameCardProps { game: Game; variant: Variant; }
const GameCard: React.FC<GameCardProps> = ({ game, variant }) => { ... };
Layout architecture
Screens are rendered inside a layout โ the screen component itself should not wrap itself in a layout.
const OrdersScreen = () => <GameDetailLayout><Panel>...</Panel></GameDetailLayout>;
Custom Hooks
Hooks live in src/hooks/. Only create one when:
- The same logic is needed in multiple components
- The hook encapsulates a genuine concern (not a one-liner wrapper)
Current hooks: useRequiredParams, useMapData, use-mobile.
Review check: used in more than one place? more than a wrapper around a single call? in /hooks/ and exported from index?
UI Guidelines
Components and icons
- Use shadcn/ui components over raw HTML
- Use Lucide icons (
lucide-react)
- Use
Notice for empty states โ never ad-hoc divs with text
- Use
ScreenCard for home screen content
Tailwind
Only add classes that actually do something. Question every class โ does it override a default that needs overriding? Is the spacing not already handled by a parent gap?
Unnecessary classes to avoid: min-w-0 (when width is already constrained), flex-shrink-0 (when already handled), h-8 w-8 (when size="icon" sets it), ml-4 (when gap handles it), h-4 w-4 on icons (when the default is fine).
Trust shadcn defaults โ size="icon" already sets button dimensions; don't override.
Review check: any Tailwind classes that don't change anything? icon sizes specified when the default is correct?
Empty states
<Notice title="No staging games" message="Go to Find Games to join a game." icon={Inbox} />
Forms
Use React Hook Form + Zod. Type is derived from schema:
const schema = z.object({ name: z.string().min(1, "Required") });
type FormValues = z.infer<typeof schema>;
const MyForm: React.FC = () => {
const form = useForm<FormValues>({ resolver: zodResolver(schema), defaultValues: { name: "" } });
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField control={form.control} name="name" render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl><Input {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
</form>
</Form>
);
};
Review check: Zod validation? type via z.infer? FormField has FormItem + FormLabel + FormControl + FormMessage? form.handleSubmit used? defaultValues for all fields?
User Feedback
Mutation toast pattern
const handleCreate = async (data: FormValues) => {
try {
await createMutation.mutateAsync({ data });
toast.success("Created successfully");
navigate("/");
} catch {
toast.error("Failed to create");
}
};
Skip toasts when the UI change itself is the confirmation (checkbox toggle, inline edit). Always use mutateAsync (not mutate) so errors propagate to the catch block.
Review check: every mutation has try/catch? success toast for non-obvious outcomes? error toast in all catch paths?
Mock Data (MSW + Fixture Registry)
cd packages/web && npm run dev:mocks
Mock mode auto-seeds auth tokens (logged in as "Mock Player"). For logged-out screens: localStorage.setItem("mock:loggedOut", "true").
Always check src/mocks/fixtures/index.ts before creating new mock data. Only add a fixture when none covers the scenario.
| Fixture | Game ID | Scenario |
|---|
pendingGameNoPlayers | pending-no-players | Pending, 0 members, joinable |
pendingGameSomePlayers | pending-some-players | Pending, 3/7 players incl. current user (creator) |
pendingGameAlmostFull | pending-almost-full | Pending, 6/7 players incl. current user |
activeGameMovement | active-movement | Spring 1901 movement; current user (England) has 2/3 orders in; chat channels with unread |
activeGameRetreat | active-retreat | Fall 1901 retreat; England army dislodged from Norway |
activeGameBuild | active-build | Fall 1901 adjustment; England can build 1 unit |
activeGameDrawProposal | active-draw-proposal | Active game with an open draw proposal, current user hasn't voted |
activeGameEliminated | active-eliminated | Active game; current user (England) has been eliminated |
activeGameCivilDisorder | active-civil-disorder | Active game; current user (England) is in civil disorder, cannot submit orders |
finishedGameSolo | finished-solo | Completed; current user won a solo victory |
finishedGameDraw | finished-draw | Completed; 3-way draw incl. current user |
gameNotJoined | not-joined | Active game the current user is not a member of |
src/mocks/ structure:
fixtures/index.ts โ registry (gameFixtures, fixtureByGameId)
fixtures/games.ts + fixtures/builders.ts โ scenarios and helpers
fixtures/classical.ts + fixtures/data/ โ real classical variant (map SVG, nation flags)
handlers.ts โ MSW request handlers; browser.ts โ worker startup + auth seeding
legacy.ts โ older mock objects still used by some unit tests/stories
Known limitation: GET /game/:id/options/ returns an empty OrderOptionsResponse โ the order creation wizard has no options under mocks. Read-only rendering (orders, maps, chat, draw proposals) is fully supported.
Screenshots for PRs
If a PR changes anything visible in the web app, you MUST take screenshots and embed them in the PR description.
Screenshots must show the component that changed. A screenshot of the wrong tab, a different variant map, or an unrelated screen does not count. Pick the fixture and navigate to the exact state where the changed component is visible. If the changed component is nested (e.g., a control on an "Advanced" tab, a non-standard map variant), navigate to that specific state โ do not settle for a screenshot that merely includes the page without the changed component in view.
cd packages/web
npm run dev:mocks &
npm run screenshot -- / /tmp/shots/home.png
npm run screenshot -- /game/active-movement/phase/101/orders /tmp/shots/orders.png
npm run screenshot -- /game/active-build/phase/303/orders /tmp/shots/mobile.png --viewport 390x844
Options: --viewport WxH (default 1280x800), --full-page, --logged-out, --wait MS, --base URL.
Never commit screenshots to the repo. Always write them to a temp path outside the working tree (/tmp/shots/, as above) and embed them in the PR description by uploading them as GitHub attachments โ do not add image files to the repo or reference them via raw.githubusercontent.com. shots/ and screenshots/ are gitignored; if you find committed screenshots, remove them.
In cloud sessions, playwright install chromium fails (CDN not allowlisted). The script auto-falls back to @sparticuz/chromium โ do not try to install Chromium manually.
Key Principles
- Component Reuse: Always check existing components before creating new ones (See ./react-components.md)
- Runtime Safety: Create and use
parseOnlyInDev for API parsing to avoid production crashes (See ./react-zod.md)
- Type Safety: Define Zod schemas first, infer TypeScript types with
z.infer (See ./react-zod.md)
- Data Loading: Use TanStack Query for new features, keep RTK Query for existing integrated features (See ./react-data-loading.md)
- Modern React: Use React 19 features โ
<Context> as provider, no forwardRef (See ./react-components.md)
- Form Excellence: React Hook Form + Zod for all new forms (See ./react-hook-form.md)
- Test Coverage: Write tests alongside features (See ./react-tests.md)
- No Rule Disabling: Fix lint violations properly โ never use
eslint-disable or @ts-ignore. The one exception is the documented mutation-in-useEffect pattern above.
- Minimize useEffect: Avoid unnecessary effects, prefer derived state and event handlers (See ./react-use-effect-minimizer.md)
Detailed Guidance Files
For comprehensive patterns and best practices, consult these detailed files:
- ./react-components.md โ Component patterns, React 19 features, Radix UI migration, compound components
- ./react-data-loading.md โ TanStack Query patterns, query keys, caching strategies, prefetching
- ./react-hook-form.md โ Form state management, validation patterns, migration from Formik
- ./react-tests.md โ Testing patterns, MSW setup, Testing Library best practices
- ./react-use-effect-minimizer.md โ When to use/avoid useEffect, refactoring patterns
- ./react-zod.md โ Zod schemas, safe parsing utility, type inference, form validation