$npx -y skills add sordi-ai/skill-everything --skill reactApply when writing React components. Hook discipline, state placement, performance, async cleanup, and list keys.
| 1 | # Sub-Skill: React Best Practices |
| 2 | <!-- target: ~2600 tokens (real tiktoken count) | 17 rules with severity classification --> |
| 3 | |
| 4 | **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. |
| 5 | |
| 6 | ## Rule classification |
| 7 | |
| 8 | - **MUST** — load-bearing. Violating causes infinite loops, stale data, leaked subscriptions, or invisible UI bugs. Never break. |
| 9 | - **SHOULD** — default behavior. Deviation needs a documented reason in the code or PR. |
| 10 | - **AVOID** — usually wrong; documented exception inline where needed. |
| 11 | |
| 12 | **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**. |
| 13 | |
| 14 | --- |
| 15 | |
| 16 | ## Component Design |
| 17 | |
| 18 | 1. **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. |
| 19 | |
| 20 | ```tsx |
| 21 | // Avoid: form state lifted to page-level parent |
| 22 | function Page() { |
| 23 | const [email, setEmail] = useState(''); |
| 24 | return <Form email={email} setEmail={setEmail} />; |
| 25 | } |
| 26 | |
| 27 | // Prefer: state lives in the component that uses it |
| 28 | function Form() { |
| 29 | const [email, setEmail] = useState(''); |
| 30 | return <input value={email} onChange={e => setEmail(e.target.value)} />; |
| 31 | } |
| 32 | ``` |
| 33 | |
| 34 | 2. **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.* |
| 35 | |
| 36 | 3. **SHOULD: Replace prop drilling beyond two levels with composition or context.** Passing `userId` through four components to reach a button is a design smell. |
| 37 | |
| 38 | ```tsx |
| 39 | // Avoid: drilling through intermediaries |
| 40 | <Layout userId={userId}><Sidebar userId={userId}><Nav userId={userId} /></Sidebar></Layout> |
| 41 | |
| 42 | // Prefer: context or render-prop composition |
| 43 | <UserContext.Provider value={userId}><Layout /></UserContext.Provider> |
| 44 | ``` |
| 45 | |
| 46 | 4. **MUST: Never create component definitions inside render.** Inner components are recreated on every render, destroying their state and forcing full remounts. |
| 47 | |
| 48 | ```tsx |
| 49 | // Wrong |
| 50 | function Parent() { |
| 51 | const Child = () => <div>hello</div>; // new reference every render |
| 52 | return <Child />; |
| 53 | } |
| 54 | |
| 55 | // Correct: define outside |
| 56 | const Child = () => <div>hello</div>; |
| 57 | function Parent() { return <Child />; } |
| 58 | ``` |
| 59 | |
| 60 | --- |
| 61 | |
| 62 | ## State Management |
| 63 | |
| 64 | 5. **MUST: Never mutate state directly.** React compares references. Mutating in place skips re-renders silently. |
| 65 | |
| 66 | ```tsx |
| 67 | // Wrong |
| 68 | const [items, setItems] = useState([]); |
| 69 | items.push(newItem); // mutation — React does not re-render |
| 70 | setItems(items); |
| 71 | |
| 72 | // Correct |
| 73 | setItems(prev => [...prev, newItem]); |
| 74 | ``` |
| 75 | |
| 76 | 6. **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. |
| 77 | |
| 78 | ```tsx |
| 79 | // Avoid |
| 80 | const [fullName, setFullName] = useState(''); |
| 81 | useEffect(() => { setFullName(`${first} ${last}`); }, [first, last]); |
| 82 | |
| 83 | // Prefer |
| 84 | const fullName = `${first} ${last}`; |
| 85 | ``` |
| 86 | |
| 87 | 7. **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. |
| 88 | |
| 89 | ```tsx |
| 90 | // Wrong: ref for displayed value |
| 91 | const count = useRef(0); |
| 92 | count.current++; // UI never updates |
| 93 | |
| 94 | // Wrong: state for a timer ID |
| 95 | const [timerId, setTimerId] = useState(null); // triggers re-render on set |
| 96 | |
| 97 | // Correct |
| 98 | const timerId = useRef(null); |
| 99 | ``` |
| 100 | |
| 101 | --- |
| 102 | |
| 103 | ## Hooks |
| 104 | |
| 105 | 8. **MUST: Specify complete dependency arrays in useEffect.** Omitting a dependency creates a stale closure. The ESLint rule `exhaustive-deps` must be enabled and respected. |
| 106 | |
| 107 | ```tsx |
| 108 | // Wrong: stale closure over userId |
| 109 | useEffect(() => { fetchUser(userId); }, []); // runs once, userId never updates |
| 110 | |
| 111 | // Correct |
| 112 | useEffect(() => { fetchUser(userId); }, [userId]); |
| 113 | ``` |
| 114 | |
| 115 | 9. **MUST: Cancel async operations in useEffect cleanup.** Fetch without an AbortController causes state updates on unmounted components and race conditions. |
| 116 | |
| 117 | ```tsx |
| 118 | useEffect(() => { |
| 119 | const controller = new AbortController(); |
| 120 | fetch(`/api/user/${id}`, { signal: controller.signal }) |
| 121 | .then(r => r.json()) |