React Style

Conventions for React code in Kevin projects, with Dedalus as the strictest production baseline.

Operating principle

React components should make state, identity, and async ownership obvious. The agent failure mode is not usually syntax. It is stale closures, racing fetches, unstable keys, derived state hidden in effects, or a UI branch that was never rendered during verification.

Prefer components that can be understood from their props, local state, query keys, and render branches. If the component needs a paragraph of explanation to avoid misuse, split the state machine or move the logic behind a typed hook.

Explicit UI State Machines

When a flow has multiple visible modes, preview those states explicitly before claiming the component is tested. The JohnPhamous artifact shows an MFA modal harness with named scenarios, avatar state buttons, error-state checkboxes, production-flow coverage notes, status cards, and an event log. That is the React testing move: enumerate the state machine users can touch, then make humans and agents drive every branch. Source: X/@JohnPhamous, 2026-05-26; Source: local video thumbnail, reviewed 2026-07-03

For production code, this does not require a heavyweight state-machine library by default. Use a typed reducer, explicit scenario fixtures, Storybook-style stories, or the Frontend and Design Skills skill when transitions matter. The invariant is that every branch is named and renderable: no MFA methods, passkey in progress, authenticator-app setup, passkey added with MFA off, MFA enabled by passkey, MFA enabled by authenticator app, and failure injection should be reachable without hand-editing live data.

Styling (Tailwind + cn(); almost no raw CSS)

  • Default: all UI via className={cn(...)} and Tailwind utilities.
  • Avoid CSS modules and co-located .css for normal components. Raw CSS belongs almost only in globals.css (or the app’s single global stylesheet) for things Tailwind cannot own: @font-face, @property, document-wide scrollbars, base layer, global keyframes, etc.
  • Do not combine lazy style={{ ... }} with Tailwind on the same node unless runtime/third-party forces it (tailwind-merge fights you).

ENFORCE (wiki): CSS UI Enforcement. Source: User, 2026-05-11

Rules of Hooks

React hooks do not have names at runtime. They are identified by position in an array. The call order must be identical on every render. If a hook appears inside an if block, it runs on some renders and not others, the array shifts, and values silently swap.

The fix is mechanical: hooks must always be called at the top level of a component or custom hook, before any conditional logic. Same order, every render. No hooks inside conditions, loops, event handlers, callbacks passed to useMemo/useReducer/useEffect, or after early returns.

The most common violation is an early return before hooks. Move hooks above the conditional.

Custom Hooks

A function whose name starts with use is a hook and follows the same rules. Name the hook after what it returns, not what it does internally. useOrgData is better than useFetchAndCacheOrgDataWithLoading.

Exhaustive Dependencies

useEffect, useMemo, and useCallback dependency arrays must include every referenced value. Missing dependencies cause stale closures. If adding a dependency causes an infinite loop, the fix is to restructure (extract to ref, memoize the dependency, or move logic to an event handler), not to suppress the warning.

setState in Effects

The React compiler flags synchronous setState inside effect bodies. The canonical alternative is useSyncExternalStore. Currently warned rather than errored; migrate incrementally.

Server/client boundary

In Next.js App Router projects, keep the server/client boundary intentional:

  • Default to Server Components for static data loading, layout, metadata, and non-interactive composition.
  • Use Client Components for stateful controls, browser APIs, realtime subscriptions, animation, and direct event handlers.
  • Pass the smallest serializable data shape across the boundary. Do not pass ORM rows, provider objects, class instances, or large blobs "just in case."
  • Keep data fetching out of useEffect unless the request is truly browser-only. Prefer server loading, TanStack Query/SWR, or a local action route with explicit cache behavior.
  • Put Suspense boundaries around work that can wait. Do not block a whole route on one slow side panel.

The boundary is a product decision. It controls latency, bundle size, cache behavior, and how much JavaScript a user pays to run.

Async Correctness and Stable IDs

These are the agent-written defects that recur in PR review (caught by Bugbot/Greptile after the fact — catch them in self-review). Source: PRs #2094, #2297, #2062

  • Discard superseded async responses. When a fetch is keyed off changing props (a range toggle, a search term), a slow earlier response can overwrite a newer one. Guard with a sequence ref (const seq = ++ref.current; ...; if (seq !== ref.current) return;) or an AbortController. On a successful refetch, clear any stale error (error: null), don't preserve it.
  • useId() for any DOM/SVG id. String ids built from data (area-fill-${dataKey}) collide when two instances share the key — SVG fill="url(#id)" then resolves to the first match and charts render the wrong gradient. Derive ids from useId().
  • Empty states must tell the truth. Distinguish "no data" from "all rows filtered out" — never show "nothing recorded yet" when records exist but are hidden by a filter.
  • No Date.now() in keys or idempotency tokens. Time-based keys defeat retry/dedup and remount stable elements. Key off stable identity.

Query and cache discipline

When using TanStack Query, SWR, or a project wrapper:

  • Query keys include every variable used by the fetcher.
  • A query function returns data. It does not call setState and return void.
  • The query client is created at app/provider scope, not inside a component body.
  • Dependency arrays should not include unstable destructured query result objects.
  • Mutations invalidate or update exactly the data they affect.
  • Optimistic updates need an undo path on failure.

Cache bugs are worse than fetch bugs because the UI looks fast while being wrong. Treat query keys as part of the public contract of the component.

Agent-Resistant Component APIs

Fardeem's frontend-architecture thread is useful because it names the agent era version of component API design: the component system can use Tailwind internally while refusing arbitrary className at its public boundary. The goal is not that Tailwind is inherently unmaintainable; the goal is to limit where agents can freestyle layout and styling. Source: X/@FardeemM self-thread, 2026-06-19

Use typed composition points instead: Row and Stack for layout, button props with named iconType variants, and compound controls such as ButtonList that accept actions: Omit<ButtonProps, "children" | "size">[]. This gives agents a small set of semantic levers to copy and extend. The caveat is local ownership: inside a component implementation, Kevin projects still default to Tailwind with className={cn(...)}; this rule is about public design-system surfaces and agent-editable APIs, not banning Tailwind from implementation code.

The same thread reinforces the state/storage split: typed query params for URL state, TanStack Query for cached server state, xstate/store or Zustand for debug settings and feature flags when context is awkward, and full state machines when a frontend is complicated enough that implicit transitions become the bug. Establish WebSocket/SSE patterns once so future agents copy an approved shape instead of inventing a new realtime path on every feature. Source: X/@FardeemM artifact reply, 2026-06-19

Match the spec at every range/branch

"Looks done" is not "to spec." Walk each mode the component supports before claiming it works: e.g. a usage chart spec of 24h=hourly, 7d=daily, 30d/90d=weekly-with-calendar-dates means the 7d branch must aggregate to daily buckets, not render 168 hourly bars. Verify the rendered output, not that the data arrived. Source: Shengming Liang, PR #2297

Agent verification loop

For any non-trivial React edit:

  1. Run the nearest fast check: typecheck, oxlint, or repo doctor.
  2. Render the changed surface in a browser.
  3. Exercise every supported branch: loading, empty, error, filtered, success, disabled, optimistic, canceled, and permission-denied when applicable.
  4. Check race behavior by changing inputs quickly while a request is in flight.
  5. Check duplicate component instances when SVG ids, DOM ids, labels, or portals are involved.
  6. Leave a test when the behavior is shared, stateful, or historically fragile.

The point is not to prove React compiled. The point is to prove the state machine the user touches is the state machine the code implements.

Linting Rules

Standard React rules are handled by Ultracite's oxlint React preset (see Ultracite & Fast Validation Recipe). The baseline includes:

Rule Severity Notes
react-hooks/rules-of-hooks error Non-negotiable, never disable
react-hooks/exhaustive-deps warn Fix stale closures, do not suppress
react/self-closing-comp error <Foo /> not <Foo></Foo>
react/jsx-curly-brace-presence error No {"text"} when text suffices

Architecture-Policy Rules

Custom mft-react ESLint rules enforce repo-specific architectural decisions that oxlint's built-in rules cannot express. These layer on top of Ultracite's baseline. See Lint-Enforced Agent Guardrails for the full table:

Rule What it catches
no-derived-state-in-effect useEffect computing derived state - use useMemo or inline render logic
no-fetch-in-effect fetch()/axios.*() inside useEffect - use TanStack Query or server-side
no-query-client-in-component new QueryClient() in component body - creates a new client per render
no-void-query-fn queryFn returning void - calling setState instead of returning data
no-unstable-query-result-deps Destructured query results in dependency arrays - new refs every render
query-key-deps queryKey missing variables used by queryFn - stale cache hits

The reviewed @ismailhoush artifact independently shows the same policy split: upstream React rules cover unstable context values, index keys, and nested components, while mft-react rules encode project architecture around derived state, browser-side fetching, QueryClient lifetime, query return values, query-result dependencies, and query-key completeness. Treat the image as corroborating evidence for the table, not a separate rule source. Source: X/@ismailhoush and local image artifact, 2026-07-04

Vercel React Best Practices

The vercel-react-best-practices skill provides prioritized performance rules from Vercel. It loads when writing React components, reviewing code, or optimizing performance. The rules above are Dedalus-specific; these cover the general React/Next.js performance frontier. Source: vercel-labs/agent-skills commit f8a72b9603728bb92a217a879b7e62e43ad76c81 and the skills.sh package page. Treat exact rule counts as registry/package details; route by the category set below.

Priority Category Impact Key Rules
1 Eliminating Waterfalls CRITICAL Promise.all() for independent ops, defer await into branches, Suspense boundaries
2 Bundle Size CRITICAL Direct imports (avoid barrel files), next/dynamic for heavy components, defer analytics
3 Server-Side HIGH React.cache() dedup, LRU cross-request cache, minimize data to client, after() for non-blocking
4 Client Data Fetching MEDIUM-HIGH SWR for dedup, deduplicate event listeners, passive scroll listeners
5 Re-render MEDIUM Extract expensive work into memoized components, derive state during render not effects, functional setState, startTransition for non-urgent updates
6 Rendering MEDIUM content-visibility for long lists, hoist static JSX, Activity component for show/hide
7 JS Performance LOW-MEDIUM Map for repeated lookups, combine filter/map into one loop, Set for O(1) lookups
8 Advanced LOW Store event handlers in refs, initialize once per app load

See Skills.sh - Agent Skills Registry for the skills-first protocol.


Timeline