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
.cssfor normal components. Raw CSS belongs almost only inglobals.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-mergefights 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
useEffectunless 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 anAbortController. 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 — SVGfill="url(#id)"then resolves to the first match and charts render the wrong gradient. Derive ids fromuseId().- 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
setStateand returnvoid. - 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:
- Run the nearest fast check: typecheck, oxlint, or repo doctor.
- Render the changed surface in a browser.
- Exercise every supported branch: loading, empty, error, filtered, success, disabled, optimistic, canceled, and permission-denied when applicable.
- Check race behavior by changing inputs quickly while a request is in flight.
- Check duplicate component instances when SVG ids, DOM ids, labels, or portals are involved.
- 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
- 2026-07-06 | Refreshed the Vercel React Best Practices reference against
vercel-labs/agent-skillsf8a72b9, routing exact performance work through Frontend and Design Skills while keeping this page focused on React ownership, state, async, and verification rules. Source: GitHubvercel-labs/agent-skills, 2026-07-06 - 2026-07-04 | Attached the @ismailhoush / @KingBootoshi React lint-policy image as evidence for the existing architecture-policy rule table: upstream React rules plus custom
mft-reactrules for derived state, fetching, QueryClient lifetime, and query-key discipline. Source: local artifactwiki/assets/x-bookmarks/2041263014200975585/image-01.png - 2026-07-03 | Added Fardeem's frontend-architecture thread as the agent-resistant component API rule: keep arbitrary styling out of public design-system boundaries, use typed composition props, and establish server-state/global-state/realtime patterns once for agents to copy. Source: X/@FardeemM, 2026-06-19
- 2026-07-03 | Added JohnPhamous's UI-as-state-machine testing bookmark as a React style rule: enumerate modal/product states in a preview harness with scenario controls, failure injection, and an event log before claiming a flow is verified. Source: X/@JohnPhamous, 2026-05-26; Source: local artifact review, 2026-07-03