Sub-Skill: React Best Practices
Purpose: Prevents the React-specific mistakes LLMs make repeatedly — wrong state placement, stale closures, unnecessary re-renders, and broken async patterns. Concrete rules with code examples.
Rule classification
- MUST — load-bearing. Violating causes infinite loops, stale data, leaked subscriptions, or invisible UI bugs. Never break.
- SHOULD — default behavior. Deviation needs a documented reason in the code or PR.
- AVOID — usually wrong; documented exception inline where needed.
Where these rules don't strictly apply: test fixtures, Storybook stories, design-system primitives in isolation, and small in-tutorial demo components may legitimately differ. The rules below apply to production application code.
Component Design
SHOULD: Co-locate state with the component that owns it. Lift state only when two siblings genuinely share it. Lifting to a grandparent "just in case" causes unnecessary re-renders across the tree.
// Avoid: form state lifted to page-level parent
function Page() {
const [email, setEmail] = useState('');
return <Form email={email} setEmail={setEmail} />;
}
// Prefer: state lives in the component that uses it
function Form() {
const [email, setEmail] = useState('');
return <input value={email} => setEmail(e.target.value)} />;
}
SHOULD: Split components at ~100 lines or when a section has its own data concern. One component = one responsibility. Extract <UserAvatar>, <OrderSummary> rather than one <ProfilePage> that does everything. Exception: pages that are mostly markup with little logic may exceed 100 lines.
SHOULD: Replace prop drilling beyond two levels with composition or context. Passing userId through four components to reach a button is a design smell.
// Avoid: drilling through intermediaries
<Layout userId={userId}><Sidebar userId={userId}><Nav userId={userId} /></Sidebar></Layout>
// Prefer: context or render-prop composition
<UserContext.Provider value={userId}><Layout /></UserContext.Provider>
MUST: Never create component definitions inside render. Inner components are recreated on every render, destroying their state and forcing full remounts.
// Wrong
function Parent() {
const Child = () => <div>hello</div>; // new reference every render
return <Child />;
}
// Correct: define outside
const Child = () => <div>hello</div>;
function Parent() { return <Child />; }
State Management
MUST: Never mutate state directly. React compares references. Mutating in place skips re-renders silently.
// Wrong
const [items, setItems] = useState([]);
items.push(newItem); // mutation — React does not re-render
setItems(items);
// Correct
setItems(prev => [...prev, newItem]);
SHOULD: Compute derived values in render, not in useEffect. If a value can be calculated from existing state/props, calculate it inline. useEffect for derived state creates a one-render lag and extra state variables.
// Avoid
const [fullName, setFullName] = useState('');
useEffect(() => { setFullName(`${first} ${last}`); }, [first, last]);
// Prefer
const fullName = `${first} ${last}`;
MUST: Use useRef for values that must not trigger re-renders (timers, DOM nodes, previous values). Use useState for anything the UI depends on. Mixing them causes invisible bugs.
// Wrong: ref for displayed value
const count = useRef(0);
count.current++; // UI never updates
// Wrong: state for a timer ID
const [timerId, setTimerId] = useState(null); // triggers re-render on set
// Correct
const timerId = useRef(null);
Hooks
MUST: Specify complete dependency arrays in useEffect. Omitting a dependency creates a stale closure. The ESLint rule exhaustive-deps must be enabled and respected.
// Wrong: stale closure over userId
useEffect(() => { fetchUser(userId); }, []); // runs once, userId never updates
// Correct
useEffect(() => { fetchUser(userId); }, [userId]);
MUST: Cancel async operations in useEffect cleanup. Fetch without an AbortController causes state updates on unmounted components and race conditions.
useEffect(() => {
const controller = new AbortController();
fetch(`/api/user/${id}`, { signal: controller.signal })
.then(r => r.json())
.then(setUser)
.catch(err => { if (err.name !== 'AbortError') setError(err); });
return () => controller.abort();
}, [id]);
MUST: Never call hooks conditionally or inside loops. Hook call order must be identical on every render. Wrap conditional logic inside the hook body, not around the hook call.
// Wrong
if (isLoggedIn) { const user = useUser(); }
// Correct
const user = useUser(); // hook always called; handle null inside
Performance
SHOULD: Wrap expensive computations in useMemo, not inline. Recalculating a sorted/filtered list on every render is the most common performance bug in React. Exception: small lists (<20 items) where the recompute is negligible — adding useMemo costs more than it saves.
// Avoid: re-sorts on every render including unrelated state changes
const sorted = items.sort((a, b) => a.name.localeCompare(b.name));
// Prefer
const sorted = useMemo(
() => [...items].sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
AVOID: New object or array literals as props inline. New references on every render break React.memo and cause child re-renders. Exception: when the consuming child is not memoised, the inline literal cost is negligible.
// Wrong: new object reference every render
<Chart options={{ color: 'red', width: 400 }} />
// Correct: stable reference
const chartOptions = useMemo(() => ({ color: 'red', width: 400 }), []);
<Chart options={chartOptions} />
SHOULD: Use React.lazy and Suspense for routes and heavy components. Bundling everything eagerly increases initial load time.
const Dashboard = React.lazy(() => import('./Dashboard'));
function App() {
return (
<Suspense fallback={<Spinner />}>
<Dashboard />
</Suspense>
);
}
Lists & Keys
MUST: Never use array index as key when the list can reorder, filter, or grow. Index keys cause React to reuse the wrong DOM nodes, breaking animations, form state, and focus. Exception: static, sort-stable lists that never filter — but stable IDs are still safer.
// Wrong
{items.map((item, i) => <Row key={i} item={item} />)}
// Correct: stable, unique identity
{items.map(item => <Row key={item.id} item={item} />)}
Testing
SHOULD: Test behavior, not implementation. Query by role/label, not by class name or component internals. Tests that break on refactors without behavior changes are noise.
// Avoid
expect(wrapper.find('.submit-btn').exists()).toBe(true);
// Prefer
expect(screen.getByRole('button', { name: /submit/i })).toBeInTheDocument();
MUST: Test loading and error states, not just the happy path. Components that render null silently on error are invisible bugs in production.
it('shows error message when fetch fails', async () => {
server.use(rest.get('/api/user', (req, res, ctx) => res(ctx.status(500))));
render(<UserProfile id="1" />);
expect(await screen.findByText(/something went wrong/i)).toBeInTheDocument();
});
SHOULD: Mock at the network boundary, not at the module boundary. Mocking fetch or using MSW keeps tests closer to real behavior than mocking useUser directly.
Responsive Design
SHOULD: Use a mobile-first approach for responsive styles. Write base styles for small screens, enhance with min-width media queries for larger viewports.
AVOID: Fixed pixel widths on container components. Use relative units or CSS logical properties. Hard-coded widths break on untested viewports. Reference: ERR-2026-015.
SHOULD: Prefer CSS container queries over viewport media queries for component-level responsiveness. Components adapt to allocated space, not full viewport.
MUST: Never assume a single breakpoint set covers all use cases. Define breakpoints based on content needs, not device names.
SHOULD: Use CSS clamp() for fluid typography and spacing instead of multiple breakpoint overrides. Scales smoothly without jumps.
Why This Sub-Skill Earns Stars
These rules target the exact failure modes that appear in LLM-generated React code: state lifted too high, effects without cleanup, index keys, inline object props, and derived state stored redundantly. Each rule is actionable in a single code review comment and includes a before/after example that makes the correct pattern unambiguous. The MUST/SHOULD/AVOID classification means hook-correctness rules are strict and stylistic rules respect context.
1---2name: react3description: Apply when writing React components. Hook discipline, state placement, performance, async cleanup, and list keys.4license: MIT5---67# Sub-Skill: React Best Practices8<!-- target: ~2600 tokens (real tiktoken count) | 17 rules with severity classification -->910**Purpose:** Prevents the React-specific mistakes LLMs make repeatedly — wrong state placement, stale closures, unnecessary re-renders, and broken async patterns. Concrete rules with code examples.1112## Rule classification1314- **MUST** — load-bearing. Violating causes infinite loops, stale data, leaked subscriptions, or invisible UI bugs. Never break.15- **SHOULD** — default behavior. Deviation needs a documented reason in the code or PR.16- **AVOID** — usually wrong; documented exception inline where needed.1718**Where these rules don't strictly apply:** test fixtures, Storybook stories, design-system primitives in isolation, and small in-tutorial demo components may legitimately differ. The rules below apply to **production application code**.1920---2122## Component Design23241. **SHOULD: Co-locate state with the component that owns it.** Lift state only when two siblings genuinely share it. Lifting to a grandparent "just in case" causes unnecessary re-renders across the tree.2526 ```tsx27 // Avoid: form state lifted to page-level parent28 function Page() {29 const [email, setEmail] = useState('');30 return <Form email={email} setEmail={setEmail} />;31 }3233 // Prefer: state lives in the component that uses it34 function Form() {35 const [email, setEmail] = useState('');36 return <input value={email} onChange={e => setEmail(e.target.value)} />;37 }38 ```39402. **SHOULD: Split components at ~100 lines or when a section has its own data concern.** One component = one responsibility. Extract `<UserAvatar>`, `<OrderSummary>` rather than one `<ProfilePage>` that does everything. *Exception: pages that are mostly markup with little logic may exceed 100 lines.*41423. **SHOULD: Replace prop drilling beyond two levels with composition or context.** Passing `userId` through four components to reach a button is a design smell.4344 ```tsx45 // Avoid: drilling through intermediaries46 <Layout userId={userId}><Sidebar userId={userId}><Nav userId={userId} /></Sidebar></Layout>4748 // Prefer: context or render-prop composition49 <UserContext.Provider value={userId}><Layout /></UserContext.Provider>50 ```51524. **MUST: Never create component definitions inside render.** Inner components are recreated on every render, destroying their state and forcing full remounts.5354 ```tsx55 // Wrong56 function Parent() {57 const Child = () => <div>hello</div>; // new reference every render58 return <Child />;59 }6061 // Correct: define outside62 const Child = () => <div>hello</div>;63 function Parent() { return <Child />; }64 ```6566---6768## State Management69705. **MUST: Never mutate state directly.** React compares references. Mutating in place skips re-renders silently.7172 ```tsx73 // Wrong74 const [items, setItems] = useState([]);75 items.push(newItem); // mutation — React does not re-render76 setItems(items);7778 // Correct79 setItems(prev => [...prev, newItem]);80 ```81826. **SHOULD: Compute derived values in render, not in useEffect.** If a value can be calculated from existing state/props, calculate it inline. useEffect for derived state creates a one-render lag and extra state variables.8384 ```tsx85 // Avoid86 const [fullName, setFullName] = useState('');87 useEffect(() => { setFullName(`${first} ${last}`); }, [first, last]);8889 // Prefer90 const fullName = `${first} ${last}`;91 ```92937. **MUST: Use useRef for values that must not trigger re-renders** (timers, DOM nodes, previous values). Use useState for anything the UI depends on. Mixing them causes invisible bugs.9495 ```tsx96 // Wrong: ref for displayed value97 const count = useRef(0);98 count.current++; // UI never updates99100 // Wrong: state for a timer ID101 const [timerId, setTimerId] = useState(null); // triggers re-render on set102103 // Correct104 const timerId = useRef(null);105 ```106107---108109## Hooks1101118. **MUST: Specify complete dependency arrays in useEffect.** Omitting a dependency creates a stale closure. The ESLint rule `exhaustive-deps` must be enabled and respected.112113 ```tsx114 // Wrong: stale closure over userId115 useEffect(() => { fetchUser(userId); }, []); // runs once, userId never updates116117 // Correct118 useEffect(() => { fetchUser(userId); }, [userId]);119 ```1201219. **MUST: Cancel async operations in useEffect cleanup.** Fetch without an AbortController causes state updates on unmounted components and race conditions.122123 ```tsx124 useEffect(() => {125 const controller = new AbortController();126 fetch(`/api/user/${id}`, { signal: controller.signal })127 .then(r => r.json())128 .then(setUser)129 .catch(err => { if (err.name !== 'AbortError') setError(err); });130 return () => controller.abort();131 }, [id]);132 ```13313410. **MUST: Never call hooks conditionally or inside loops.** Hook call order must be identical on every render. Wrap conditional logic inside the hook body, not around the hook call.135136 ```tsx137 // Wrong138 if (isLoggedIn) { const user = useUser(); }139140 // Correct141 const user = useUser(); // hook always called; handle null inside142 ```143144---145146## Performance14714811. **SHOULD: Wrap expensive computations in useMemo, not inline.** Recalculating a sorted/filtered list on every render is the most common performance bug in React. *Exception: small lists (<20 items) where the recompute is negligible — adding useMemo costs more than it saves.*149150 ```tsx151 // Avoid: re-sorts on every render including unrelated state changes152 const sorted = items.sort((a, b) => a.name.localeCompare(b.name));153154 // Prefer155 const sorted = useMemo(156 () => [...items].sort((a, b) => a.name.localeCompare(b.name)),157 [items]158 );159 ```16016112. **AVOID: New object or array literals as props inline.** New references on every render break React.memo and cause child re-renders. *Exception: when the consuming child is not memoised, the inline literal cost is negligible.*162163 ```tsx164 // Wrong: new object reference every render165 <Chart options={{ color: 'red', width: 400 }} />166167 // Correct: stable reference168 const chartOptions = useMemo(() => ({ color: 'red', width: 400 }), []);169 <Chart options={chartOptions} />170 ```17117213. **SHOULD: Use React.lazy and Suspense for routes and heavy components.** Bundling everything eagerly increases initial load time.173174 ```tsx175 const Dashboard = React.lazy(() => import('./Dashboard'));176177 function App() {178 return (179 <Suspense fallback={<Spinner />}>180 <Dashboard />181 </Suspense>182 );183 }184 ```185186---187188## Lists & Keys18919014. **MUST: Never use array index as key when the list can reorder, filter, or grow.** Index keys cause React to reuse the wrong DOM nodes, breaking animations, form state, and focus. *Exception: static, sort-stable lists that never filter — but stable IDs are still safer.*191192 ```tsx193 // Wrong194 {items.map((item, i) => <Row key={i} item={item} />)}195196 // Correct: stable, unique identity197 {items.map(item => <Row key={item.id} item={item} />)}198 ```199200---201202## Testing20320415. **SHOULD: Test behavior, not implementation.** Query by role/label, not by class name or component internals. Tests that break on refactors without behavior changes are noise.205206 ```tsx207 // Avoid208 expect(wrapper.find('.submit-btn').exists()).toBe(true);209210 // Prefer211 expect(screen.getByRole('button', { name: /submit/i })).toBeInTheDocument();212 ```21321416. **MUST: Test loading and error states, not just the happy path.** Components that render `null` silently on error are invisible bugs in production.215216 ```tsx217 it('shows error message when fetch fails', async () => {218 server.use(rest.get('/api/user', (req, res, ctx) => res(ctx.status(500))));219 render(<UserProfile id="1" />);220 expect(await screen.findByText(/something went wrong/i)).toBeInTheDocument();221 });222 ```22322417. **SHOULD: Mock at the network boundary, not at the module boundary.** Mocking `fetch` or using MSW keeps tests closer to real behavior than mocking `useUser` directly.225226---227228## Responsive Design22923018. **SHOULD: Use a mobile-first approach for responsive styles.** Write base styles for small screens, enhance with `min-width` media queries for larger viewports.23123219. **AVOID: Fixed pixel widths on container components.** Use relative units or CSS logical properties. Hard-coded widths break on untested viewports. Reference: ERR-2026-015.23323420. **SHOULD: Prefer CSS container queries over viewport media queries for component-level responsiveness.** Components adapt to allocated space, not full viewport.23523621. **MUST: Never assume a single breakpoint set covers all use cases.** Define breakpoints based on content needs, not device names.23723822. **SHOULD: Use CSS `clamp()` for fluid typography and spacing instead of multiple breakpoint overrides.** Scales smoothly without jumps.239240---241242## Why This Sub-Skill Earns Stars243244These rules target the exact failure modes that appear in LLM-generated React code: state lifted too high, effects without cleanup, index keys, inline object props, and derived state stored redundantly. Each rule is actionable in a single code review comment and includes a before/after example that makes the correct pattern unambiguous. The MUST/SHOULD/AVOID classification means hook-correctness rules are strict and stylistic rules respect context.