React
Agent skills that give AI agents the operational competence of expert practitioners (Salesforce, AWS, GitHub Actions, web). SKILL.md format; Claude Code plugins. Not test-prep — certification is the scaffold and benchmark, not the product.
npx -y skills add toddkasper/expert-skills --skill reactAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Building and reviewing React applications — components and JSX, hooks (useState/useReducer/useEffect/useRef/useContext, custom hooks), the rules of hooks, state and data flow, rendering and memoization, concurrent features, accessibility, and testing with React Testing Library. Use when writing, reviewing, or debugging React UIs, re-renders/effects, or state architecture. Excludes Next.js (see nextjs) and React Native/Expo (see react-native). Competence skill anchored on react.dev — no first-party certification.
SKILL.md
37.0 KB, as published. Nobody here has run it
React — Skills Reference
Overview
No vendor certification exists for React. This playbook encodes the working competence applied when building, reviewing, or debugging a React application — anchored entirely on official React documentation (react.dev) and broadly accepted community best practices. Anchor all claims to react.dev; verify against it before acting.
Scope of this skill: React itself — components, hooks, state, rendering, data-fetching patterns, accessibility, and testing. Framework concerns (Next.js App Router, Server Components, Expo) are deferred to the nextjs and react-native sibling skills.
Load this skill when… writing or reviewing React components, hooks, or state architecture; debugging re-renders, stale closures, or effect dependencies; auditing accessibility (ARIA, focus management); writing or fixing React Testing Library tests. Not this skill: Next.js App Router, Server/Client Components, Server Actions → see
nextjs; React Native / Expo mobile apps → seereact-native.
Study resources and data-fetching library links live in references/study-resources.md.
Verify steps assume nothing about your tooling — use your project's own scripts and the language toolchain (
tsc,node, the test runner, the package manager), in that order of preference.
Uncertainty & Escalation
- Always re-verify live: React evolves rapidly — hooks, concurrent features, and the React Compiler change behavior across minor versions.
[volatile — verify live]marks apply to: React Compiler availability and defaults (production-ready late 2025 — check the project'sbabel-plugin-react-compilerversion before assuming it is active); compiler lint rules —eslint-plugin-react-compileris superseded byeslint-plugin-react-hooksv6, which folds in the compiler rules (React 19.2, Oct 2025);useActionStateanduseOptimistic(React 19+ only); React 18 batching behavior in non-React-managed event handlers (verify with React 18+); TanStack Query and SWR API surface (major-version breaking changes — always check installed version). Checkreactinpackage.json— React 18 and 19 differ on form actions, optimistic updates, and compiler support. - Live wins: the installed React version's actual behavior and react.dev are authoritative over this file → log discrepancies via Feedback protocol below.
- Escalate to a human: upgrading from React 17→18 or 18→19 in a production app (concurrent mode and batching changes can surface latent bugs); removing
StrictModefrom a production app; major data-fetching library upgrades (TanStack Query v4→v5 API changes); production deploys. - Confidence taxonomy: facts in this file are stable unless tagged
[volatile — verify live]or[opinion — house style].
1. Components, Hooks & State
Component fundamentals
- Every component must be a pure function of its inputs (props + state). Same inputs → same JSX output, every time. Side effects in the render body (mutations, subscriptions, timers) are bugs waiting to surface under Strict Mode's double-invoke or the React Compiler.
- Never define a component inside another component. Each render call creates a new function reference; React sees it as a different component type, unmounts the inner component, and resets its state. Define all components at module top level.
- Key correctness. Keys tell React which items in a list are the same across renders. Use stable IDs (database id, slug); never use array index as a key on a list that can be reordered, filtered, or have items inserted — React will misapply state and DOM attributes.
Rules of hooks (non-negotiable)
- Call hooks only at the top level of a React function — never inside loops, conditionals, or early returns.
- Call hooks only from React function components or from other custom hooks, never from plain functions.
Violating either rule makes hook call order unstable across renders; React will corrupt state. ESLint's eslint-plugin-react-hooks (v6+ in React 19.2 projects, which folds in the former eslint-plugin-react-compiler rules) enforces both rules automatically — enable and trust it. See react.dev/blog/2025/10/01/react-19-2.
useState — right tool, right scope
- Don't sync derived values into state with an effect. If a value can be computed from existing state or props, compute it during render:
const fullName = firstName + ' ' + lastName. Storing it in state and synchronizing it viauseEffectcreates a double-render and is a category of bug, not a pattern. - Lift state to the closest common ancestor of all components that need it. Over-lifting (global state for local concerns) creates unnecessary coupling and re-renders.
- Don't mirror props into state at initialization unless you explicitly intend to decouple updates. If you do decouple, name the variable to signal it:
const [editedTitle, setEditedTitle] = useState(initialTitle).
Prop-mirroring trap: when a child's state should reset on a controlling prop change, two correct approaches:
| Situation | Correct fix |
|---|---|
| Child should reset all state when prop changes | Give the child a key equal to the controlling prop from the parent: <TabContent key={tab} />. React unmounts and remounts the child cleanly — no effect needed. |
| Child should be fully controlled (no local selection state) | Remove the local state entirely; accept the value as a prop. |
Wrong: useEffect(() => { setState(prop); }, [prop]) — still fires a double render (stale state visible before the effect corrects it). Guard conditions inside the effect have the same problem. The key prop or a fully-controlled pattern eliminates this effect entirely.
useReducer — prefer over useState when next state depends on previous in complex ways, multiple fields change together as a unit, or you want pure update logic outside the component for testability. Reducer functions are pure: state + action → next state, no side effects.
useEffect — the discipline
useEffect synchronizes a component with an external system (DOM APIs, browser subscriptions, third-party libraries, network). If there is no external system, you don't need an effect.
Dependency array rules:
- Every reactive value read inside the effect (props, state, variables derived from them) must appear in the deps array. Omitting deps to "run less" is a stale-closure bug.
- Don't add deps to suppress the lint warning — instead, restructure the effect to need fewer reactive values (move objects/functions inside the effect, or use
useReducerdispatch which is stable). - An empty dep array
[]means "run once after mount and clean up on unmount" — correct for subscriptions, wrong for effects that should react to prop/state changes.
Always return a cleanup function when the effect sets up a subscription, timer, or connection. Without cleanup, re-mounting, StrictMode double-invoke, or hot reload will leak.
useEffectEvent — non-reactive values in effects: Stable since React 19.2 (Oct 2025). When an effect needs to read a prop or state value (e.g., a logging callback or analytics flag) without re-running the effect every time that value changes, wrap the non-reactive read in useEffectEvent. The event function always sees the latest value but is not a reactive dependency — you do not add it to the deps array. This replaces the previous workaround of holding the value in a useRef and reading ref.current inside the effect. Source: react.dev/reference/react/useEffectEvent.
Common effect over-use patterns — and the fix:
| Mistaken pattern | Fix |
|---|---|
useEffect updates state from props | Compute derived value during render; or use key prop to reset the component |
useEffect sends a POST after state flag | Call the API in the event handler directly |
| Chain of effects updating each other | Collect all state changes in the event handler; compute final state together |
useEffect notifies parent via callback | Call the parent callback directly in the event handler |
useEffect transforms data for render | Calculate during render (or useMemo if expensive) |
useRef — holds a mutable value that doesn't trigger re-renders. Use for: DOM node references, timer IDs, tracking mount state, previous prop values. Never read or write refs during rendering.
useContext — eliminates prop-drilling for cross-cutting values (theme, locale, current user). Not a replacement for server-state caching — use TanStack Query or Zustand for that. Every subscriber re-renders on context value changes: split contexts by change frequency and memoize the provider value object.
Custom hooks: a function whose name starts with use that calls other hooks. Extract shared stateful logic into custom hooks; each call gets its own independent state instance. Extended guidance: references/advanced-patterns.md → "Custom Hooks."
2. Rendering & Performance
How React renders
Rendering = calling your component function to produce a JSX tree. React reconciles it against the previous tree and commits only changed DOM nodes. Three phases: Trigger → Render → Commit.
State updates are batched in React 18+. Multiple setState calls in a single event handler are batched into a single re-render.
Memoization: don't memoize without measuring. In React Compiler projects [volatile — verify live], the compiler inserts React.memo/useMemo/useCallback automatically — manual memoization is usually redundant. Without the compiler, apply after profiling: React.memo memoizes render output, useMemo memoizes a computed value, useCallback memoizes a function reference (only useful when passed to a memo-wrapped child). Prefer structural patterns first: accept children as a prop, keep state local, split contexts by change frequency. Full comparison table and when-to-apply criteria: references/advanced-patterns.md → "Memoization."
Key prop — the reset lever
Giving a component a different key tells React to unmount the old instance and mount a new one, resetting all its state. This is the correct fix for "reset form when user changes" — <Form key={userId} /> — not an effect that clears state fields.
Concurrent features (React 18+) — load references/advanced-patterns.md → "Concurrent Features" for useTransition, useDeferredValue, and Suspense usage patterns. Key constraint: useTransition cannot be used to control text inputs (react.dev is explicit). For a laggy controlled filter/text input, the correct tool is useDeferredValue + memo on the expensive consumer, or a two-state split — not useTransition.
Profiling
Use React DevTools Profiler before optimizing. It shows which components rendered, why, and how long. Optimize the actual bottleneck, not the assumed one.
Red flags in review:
useMemo/useCallbackapplied everywhere without measurement- Component definitions nested inside other component render bodies
- Array index used as
keyon a sortable/filterable list useEffectreading a value not in the dep array (stale closure)useStateholding a value that's derivable from other state- Multiple sequential
setStatecalls that each force a re-render (batch or useuseReducer)
3. Data & Architecture
Data fetching
React itself has no built-in data-fetching — use a library that handles caching, background refresh, loading/error states, and deduplication.
| Need | Tool |
|---|---|
| Server state (remote data, REST/GraphQL) | TanStack Query (React Query) or SWR |
| Optimistic updates before server confirmation | useOptimistic (React 19) or TanStack Query's optimisticUpdate |
| Form submission with loading/error state | useActionState (React 19) replaces the useState + onSubmit + loading flag pattern |
| Client/global UI state | useState / useReducer + context, or Zustand |
| Complex cross-component state | Redux Toolkit or Zustand |
Avoid writing useEffect + fetch + useState for server data — you get no caching, no deduplication, and race conditions. Use TanStack Query or SWR instead.
Race condition: when a component fetches data on a changing prop, a slow response from an old request can overwrite the new response. Fix: use the effect cleanup to set an ignore flag, or use a library that handles this automatically. Example pattern: references/advanced-patterns.md → "Race Condition."
State architecture decisions
| Question | Guidance |
|---|---|
| Does only one component need this? | Keep it local with useState |
| Do sibling components need the same data? | Lift to closest common ancestor |
| Is it remote/server data that must be cached? | TanStack Query / SWR, not context |
| Is it shared UI state across many unrelated components? | Context (if low-frequency updates) or Zustand |
| Is the update logic complex or shared? | useReducer with context, or Redux Toolkit |
Folder / feature structure and framework scope boundary (Next.js/RN handoff rules, feature-based co-location guidance): references/advanced-patterns.md → "Folder Structure and Framework Scope."
React 19 and 19.2 Notable Additions
These features are in scope for this skill. They shipped across two releases — React 19.0 (Dec 2024) and React 19.2 (Oct 2025) — so version attribution matters when checking availability.
React 19.0 (2024-12-05) — source: https://react.dev/blog/2024/12/05/react-19
use()hook — reads a resource (a Promise or a Context) during render.use(SomeContext)replacesuseContext(SomeContext).use(promise)suspends until the promise resolves (must be inside a Suspense boundary). Unlike hooks,use()can be called conditionally. Use for data-loading patterns that pair with Suspense; preferuseContextfor simple context reads (more explicit).refas a prop — function components now acceptrefdirectly as a prop (e.g.function Input({ ref, ...props })).forwardRefis no longer needed for new components. ExistingforwardRefusage continues to work but is considered legacy.<Context>as a provider — render<MyContext value={…}>directly;<MyContext.Provider value={…}>still works but is no longer required.
React 19.2 (2025-10-01) — source: https://react.dev/blog/2025/10/01/react-19-2
<Activity>component — hides/shows a subtree without unmounting it, preserving state while the subtree is visually hidden. Intended for tabs, expandable panels, and off-screen pre-rendering. Use instead of CSSdisplay:nonewhen you need preserved state across visibility changes.- Performance Tracks in DevTools — React DevTools 19.2 adds a "Performance Tracks" panel in the browser profiler timeline, visualizing when React components render, commit, and suspend alongside native browser tasks. Use it to pinpoint jank sources more precisely than the Profiler flame chart alone.
4. Accessibility & Testing
Accessibility
React renders to the DOM — all standard HTML accessibility rules apply. React-specific concerns:
- Prefer semantic HTML. A
<button>is keyboard-focusable and announces its role to screen readers automatically. A<div onClick>is not. Use the correct element before reaching for ARIA. - Every interactive element needs an accessible name. Buttons and links must have visible text or
aria-label/aria-labelledby. Icon-only buttons (no visible text by design) must havearia-label— that is the correct and complete fix for that case. aria-labelis not a substitute for a visible<label>. For form inputs, the correct fix is a persistent visible label (<label htmlFor="id">).aria-labelsatisfies screen readers but leaves sighted/cognitive-accessibility users without a persistent field name. Reservearia-labelfor icon-only buttons; on form fields it is a lower-quality fix. Decision table: references/advanced-patterns.md → "Accessibility: Input Accessible Name."- Associate form inputs with labels. Use
<label htmlFor="inputId">(htmlFor, notforin JSX) or wrap the input inside the label. Never rely on placeholder text alone. - Focus management on dynamic content. When a modal opens, move focus inside it; trap Tab within the modal; when it closes, return focus to the trigger. Use
useRef+element.focus(). For lists that update (e.g. after a filter), ensure the user's position is not lost. - ARIA roles supplement, not replace, semantics. Add
role,aria-expanded,aria-controls,aria-livewhen native HTML can't convey the interaction. Anaria-live="polite"region announces dynamic updates (search results count, toast notifications) to screen readers without moving focus. - Keyboard navigation. Every action available by mouse must be reachable by keyboard. Tab/Shift-Tab navigate focusable elements; Enter/Space activate buttons; Escape dismisses overlays. Custom widgets (combobox, date picker, slider) must implement the ARIA Authoring Practices patterns.
Red flags in review:
onClickon a non-interactive element with noroleortabIndex<img>withoutalt(oralt=""for decorative images)<input>without associated<label>- Focus not managed after modal open/close
- Color alone used to convey meaning
Testing with React Testing Library
Core principle: test what the user sees and does, not what the component stores internally. Tests that assert on state variables or component internals break on refactor while behavior stays the same.
Query priority: getByRole (most robust; validates a11y) → getByLabelText → getByPlaceholderText (fix the a11y issue too) → getByText → getByTestId (last resort; use data-testid, not class names).
Interaction: use @testing-library/user-event (userEvent.click, userEvent.type) rather than fireEvent. userEvent simulates the full browser event sequence (pointerdown, mousedown, focus, click, etc.); fireEvent fires a single synthetic event and misses side effects.
Async patterns:
findBy*queries return a Promise and retry until the element appears or times out — use for anything that renders after an async operation.waitFor(() => expect(...))retries the assertion — use when multiple things must be true after an async operation.- Never use
act()manually in tests that useuserEventv14+ — it wraps automatically.
What not to test:
- Internal state values (test what the UI shows instead)
- Exact CSS classes (test behavior, not styling implementation)
- That a mock was called with internal component details (test the resulting UI, not the wiring)
Custom hooks: test via renderHook from @testing-library/react. Invoke the hook, manipulate inputs via act(), assert on the returned values.
Decision scenario:
Situation: A button triggers a network call, and after success a confirmation message appears. The test clicks the button and immediately asserts on the message — but the assertion fails. Competent move: Switch to
await screen.findByText('Confirmation')or wrap the assertion inawait waitFor(() => expect(screen.getByText('Confirmation')).toBeInTheDocument()). The message renders asynchronously;getBy*queries are synchronous and fail immediately. Tempting-but-wrong: Addingawait act(async () => userEvent.click(button))and then a synchronousgetBy*. This works in some cases but is fragile and masks the real intent. Verify: Run the test with--verbose; confirm the query waits and passes when the message appears.
Executable Workflows
Workflow 1 — Place state correctly (compute-during-render vs lift vs key-reset — avoid effect-sync)
- Ask: "Can this value be derived from existing state or props?" If yes, compute it during render — no
useState, nouseEffect. → gate: removing the derived state and computing inline produces identical UI with no console warnings. - Ask: "Does only one component need this value?" If yes, keep it local with
useStatein that component. Move on. - Ask: "Do two sibling components need the same value?" Lift state to their closest common ancestor. Pass as props. → gate: both siblings render the same value; changing it in one sibling updates the other.
- Ask: "Should a child reset all its state when a controlling prop changes?" Give the child a
keyequal to the controlling prop:<Form key={userId} />. Do not writeuseEffect(() => { setState(prop) }, [prop]). → gate: change the controlling prop; confirm the child's internal state resets cleanly (React DevTools Components tab shows the instance unmounted and remounted). - Ask: "Is this server/remote data?" Use TanStack Query or SWR — not
useEffect + fetch + useState. → gate: remove theuseEffect+fetch; queries deduplicate, cache, and handle loading/error states automatically.
Workflow 2 — Diagnose an extra/infinite re-render (profiler → identity/deps → memo decision)
- Open React DevTools Profiler. Record a session covering the unexpected re-render. Click the component that re-renders unexpectedly — the "Why did this render?" panel lists the prop or state that changed. → gate: you can name the specific prop/state/context value that triggered the render.
- If the triggering value is an object or function, check whether its reference is stable. Inline
{}and() => {}in render produce a new reference every render. → gate:console.log(Object.is(prev, next))(or React DevTools) confirms the reference is changing. - If the reference instability is in a context value, wrap with
useMemo(() => ({ … }), [deps])at the provider. If it is a callback prop, wrap withuseCallback(fn, deps)at the parent. → gate: re-record in Profiler; the component should no longer appear in the flame chart for unrelated parent renders. - For infinite render loops: check whether a
useEffectsets state unconditionally (no condition beforesetState) or whether its deps array contains an unstable object that it also updates. → gate: enableeslint-plugin-react-hooks—exhaustive-depswarns on missing/extra deps; fix the root cause, not the warning. - Only add
React.memoafter confirming the re-render is measurably slow (Profiler "render duration" > 2ms under real load). In React Compiler projects, skip manual memo entirely. → gate: Profiler shows reduced render count without introducing stale prop bugs.
Workflow 3 — Test a component's behavior with React Testing Library
- Render the component with
render(<MyComponent …/>). Import from@testing-library/react. - Query for elements with the highest-confidence query first:
getByRole('button', { name: /submit/i }), thengetByLabelText, thengetByText, nevergetByClassName. → gate: the query finds exactly one element; if it throws "Unable to find…", runscreen.debug()to inspect the rendered tree. - Interact using
@testing-library/user-event:await userEvent.click(button),await userEvent.type(input, 'hello'). Do not usefireEventunless you need low-level synthetic events. → gate:userEvent.setup()is called once per test (v14+ API); all interactions are awaited. - For async outcomes (data loaded, UI updated after an API call), use
await screen.findByText('Expected text')orawait waitFor(() => expect(screen.getByRole('alert')).toBeInTheDocument()). Never query synchronously for async results. → gate: the test passes consistently without artificialsetTimeoutorsleepcalls. - Assert on what the user sees, not on internal state.
expect(screen.getByRole('status')).toHaveTextContent('Saved')— notexpect(component.state.saved).toBe(true). → gate: refactoring the component's state shape without changing visible behavior does not break the test.
Decision Scenarios
Scenario 1 — Context provider value object recreated every render causes all consumers to re-render
Situation: A
ThemeProvidercomponent holds{ theme, setTheme }as a context value created inline:<ThemeContext.Provider value={{ theme, setTheme }}>. Profiling reveals that every component consuming the context re-renders whenever any state anywhere in the app changes — even unrelated state that re-renders theThemeProviderparent.
Competent move: Memoize the context value:
const value = useMemo(() => ({ theme, setTheme }), [theme, setTheme]). The inline{}creates a new object reference on every render, and React's context compares values by reference — all subscribers see a "changed" value and re-render. Memoizing the object ensures consumers only re-render whenthemeorsetThemeactually changes.
Tempting-but-wrong: Wrapping each consumer component in
React.memo.React.memoskips re-renders due to unchanged props, but context changes are not props —React.memodoes not prevent context-triggered re-renders. The fix must be at the provider, not the consumer.
Verify: Use React DevTools Profiler before and after the fix. Before: every context consumer shows a re-render reason of "context changed" on every unrelated state update. After memoization: consumers only re-render when
themeitself changes.
Scenario 2 — useEffect cleanup missing from a setInterval in a polling component
Situation: A
LivePriceTickercomponent starts asetIntervalthat fetches a price every 3 seconds inside auseEffectwith an empty dependency array[]. In development (Strict Mode), multiple fetch calls fire simultaneously on mount, creating duplicate network requests. In production the component unmounts and remounts during navigation, and after navigating away the polling continues in the background.
Competent move: Return a cleanup function from the effect:
return () => clearInterval(intervalId). React Strict Mode double-invokes effects in development to surface missing cleanup — the duplicate requests are the diagnostic signal, not a bug to suppress. The cleanup function runs on unmount and on every re-run in Strict Mode, stopping the interval correctly in both environments.
Tempting-but-wrong: Disabling Strict Mode to stop the duplicate requests in development. Strict Mode is revealing a real production bug (the leaked interval after unmount). Removing it hides the signal without fixing the problem.
Verify: Wrap the component in a toggle button so it can be mounted and unmounted. Without cleanup, open the browser Network tab and confirm requests continue after the component is hidden. With cleanup, requests stop immediately on unmount.
Scenario 3 — useActionState for form submission vs manual useState + onSubmit loading flag
Situation: A React 19 project has a comment-submission form implemented with three
useStatevariables (value,isLoading,error) and ahandleSubmitfunction that sets them manually. A reviewer suggests replacing the whole pattern withuseActionState.
Competent move: Replace the three-variable pattern with
const [state, submitAction, isPending] = useActionState(postCommentAction, initialState).useActionState(React 19) encapsulates the loading/error/result lifecycle for an async action, provides a stableisPendingboolean, and supports progressive enhancement when used with<form action={submitAction}>. The manual pattern is error-prone (forgetting to resetisLoadingon error is a common bug) and requires more code.
Tempting-but-wrong: Using
useReducerto consolidate the three state fields.useReducerremoves the multi-setState problem but still requires manualisLoadingmanagement and does not integrate with<form action>for progressive enhancement.useActionStateis the purpose-built React 19 primitive for this exact pattern.
Verify: Confirm the project is on React 19+ (
package.json). ImplementuseActionStateand verifyisPendingbecomestrueduring the async call andfalseafter — test with a slow mock action usingsetTimeoutinside a wrapper.
Scenario 4 — Testing a custom hook with renderHook and act — asserting synchronous state
Situation: A developer tests a custom
useCounterhook by callingrenderHook(() => useCounter(0))and immediately assertingresult.current.count === 0. The test passes. They then call theincrementfunction and assertresult.current.count === 1— but the assertion fails because the value is still0.
Competent move: Wrap the state-mutating call in
act():act(() => { result.current.increment(); }). React batches and flushes state updates asynchronously.act()tells React Testing Library to flush all pending state updates and effects before the assertion runs, soresult.current.countreflects the post-update value.
Tempting-but-wrong: Using
waitFor(() => expect(result.current.count).toBe(1)).waitForis for asynchronous operations (network calls, timers). A synchronous state update triggered by calling a hook function should be wrapped inact(), not awaited — usingwaitForhere works but signals a misunderstanding of what is synchronous vs asynchronous.
Verify: Run the test with
act()wrapping theincrementcall. The assertion should pass without any timeout. Also verify that omittingact()produces a React testing warning about "not wrapped in act" in the console — that warning is the diagnostic signal for this pattern.
Operational Rules Quick Reference
Read this before writing or reviewing any React code.
- DO keep components pure — same props + state → same JSX, every render, no side effects in the render body.
- DON'T define components inside other components — each render creates a new reference; React unmounts and resets state.
- DO place state at the closest common ancestor of the consumers; no higher.
- DON'T sync derived values into state via
useEffect— compute them during render. - DO include every reactive value read inside an effect in the deps array. Never omit deps to reduce runs.
- DON'T write
useEffect+fetch+useStatefor server data — use TanStack Query or SWR. - DO return a cleanup function from every effect that sets up a subscription, timer, or connection.
- DON'T use array index as
keyon any list that can be reordered, filtered, or have items inserted. - DO use
keyprop to intentionally reset a component's state (e.g.<TabContent key={tab} />,<Form key={userId} />). This is the preferred fix when a child's state should reset on a controlling prop change — not auseEffectthat mirrors the prop into state. - DON'T use
useEffectto sync a prop into local state (useEffect(() => { setState(prop) }, [prop])). The component renders once with stale state before the effect fires. Usekeyto remount or make the component fully controlled instead. - DON'T treat
aria-labelas a substitute for a visible<label>on form inputs — it satisfies screen readers but leaves sighted users without a persistent label. Use a visible label element for inputs; reservearia-labelfor icon-only buttons and other cases where no visible label text is appropriate. - DON'T memoize (
React.memo/useMemo/useCallback) without measuring first — in React Compiler projects, the compiler handles this automatically. - DO use
useTransition(startTransition) to keep the UI responsive during expensive non-urgent state updates — but not to fix a laggy controlled text input. react.dev is explicit: "Transitions can't be used to control text inputs." For a laggy controlled input, useuseDeferredValue(defer the derived/expensive work while keeping the input controlled) plusmemoon the expensive consumer — or a two-state split. - DO use
useDeferredValueto defer the expensive re-render of a memoized child component while keeping the controlled input immediately responsive. Note:useDeferredValueonly helps when the expensive consumer is wrapped inReact.memo; withoutmemothe deferred value still triggers a synchronous re-render. - DO use semantic HTML first (
<button>,<label htmlFor>,<nav>,<main>) before reaching for ARIA. - DON'T put
onClickon a non-interactive element withoutroleandtabIndex. - DO manage focus programmatically when modals open/close or content changes significantly.
- DO query with
getByRolefirst in RTL tests; fall back togetByTestIdonly as a last resort. - DON'T test internal state or implementation details — test what the user sees and can do.
- DO use
findBy*orwaitForfor anything that renders after an async operation. - DO use
userEvent(notfireEvent) for interaction in tests. - DO verify against official react.dev documentation before treating any of the above as ground truth — React evolves.
Feedback protocol
Using this skill and hit a wall? If you find a claim contradicted by the live system or official docs, a missing rule that cost you a wrong attempt, or a decision this skill gave no criteria for — append an entry in the moment to .skill-feedback/react.md at the project root (create it if absent):
date | skill last-reviewed | claim or gap | what you observed instead | evidence (error text / doc URL / query output) | suggested fix
These are harvested back into the skill via the learning loop. When the live system and this file disagree, trust the live system.
Changelog
- 2026-06-09 — Conformed to the 12-dimension skill standard: task-vocab description + Scope block, Uncertainty & Escalation guidance with inline
[volatile — verify live]marks, executable workflows, tool-agnostic verify steps, and the feedback protocol above.last-reviewedset to 2026-06-09. - 2026-06-09 — Curation pass (inbox: D9 audit finding): inlined 3 decision scenarios into the body (Scenarios 2–4: useEffect cleanup/Strict Mode, useActionState vs useState, renderHook/act) to meet the teaching-scenario standard (≥4 inline). references/scenarios.md deleted (all scenarios now inline). Custom hooks, Concurrent features, race-condition code block, and Folder/framework scope subsections moved to references/advanced-patterns.md to offset body length.
- 2026-06-10 — Curation pass C3 (inbox 2026-06-10). Six findings integrated: (1) useTransition/controlled-input correctness bug — corrected guidance: useTransition cannot be used to control text inputs (react.dev); useDeferredValue+memo is the right tool; updated Operational Rules and Section 2 pointer; eval answer-key scenario 6 rewritten. (2) useEffectEvent added — new paragraph in useEffect section covering stable React 19.2 API for non-reactive deps in effects. (3) useDeferredValue mischaracterization fixed — corrected in references/advanced-patterns.md: not a debounce/timer; immediate interruptible background re-render; only helps with memo'd consumer; added tool-selection table. (4) Stale tooling —
eslint-plugin-react-compilersuperseded byeslint-plugin-react-hooksv6 (React 19.2); updated Uncertainty section and Rules of Hooks paragraph. (5) Coverage gaps — added React 19/19.2 Notable Additions section:use()hook, ref-as-prop, Context-as-provider,<Activity>component, Performance Tracks in DevTools. (6) Trademark + link — disclaimer updated from Meta to React Foundation (Linux Foundation, Feb 2026); Rules-of-Hooks link in study-resources.md corrected to react.dev/reference/rules/rules-of-hooks; study-resources footer updated. Eval: situations.md scenario 6 prompt clarified for coherence with corrected answer; 6 new held-out scenarios added (13–18) probing useEffectEvent, useDeferredValue+memo, Activity, use(), ref-as-prop, and useTransition/controlled-input distinctions. - 2026-06-10 — Cycle-3 volatile reconciliation: 6 facts confirmed (markers cleared); corrected React 19.0 vs 19.2 attribution for use()/ref-as-prop/Context-as-provider (19.0) vs Activity/Performance Tracks (19.2).
Independent educational content for upskilling AI agents. React is a trademark of the React Foundation (a Linux Foundation project) — see react.dev/blog/2026/02/24/the-react-foundation. Not affiliated with, authorized by, endorsed by, or sponsored by the React Foundation or any contributor. All guidance is provided as-is; verify against official documentation and the live react.dev site before acting. No credential or certification outcome is implied.