React Hooks Discipline
Cursor.cursor/rules/*.mdcCoding
Hard rules for effect dependencies, deriving state instead of syncing it, and when code earns extraction into a custom hook.
Use when: Attach to TSX files in any React project to stop effect overuse, stale-closure bugs, and premature custom-hook abstraction.
Get this skill
Add the skill file with the CLI, by hand, or over MCP.Save this to your project or home directory so Cursor can load it.
npx goodprompts add react-hooks-disciplineSaves to
./.cursor/rules/react-hooks-discipline.mdcnpx goodprompts add react-hooks-disciplineSaves to
./.cursor/rules/react-hooks-discipline.mdcSkill file
.cursor/rules/*.mdc3,213 characters---
description: Rules for useState/useEffect/useMemo usage and custom hook extraction
globs: "**/*.tsx"
alwaysApply: false
---
# React hooks rules
## Derived state over effects
- If a value can be computed from props or existing state during render, compute it during render. Never mirror it into state with a syncing effect.
```tsx
// BAD: const [fullName, setFullName] = useState(""); useEffect(() => setFullName(`${first} ${last}`), [first, last]);
// GOOD: const fullName = `${first} ${last}`;
```
- Wrap a derived value in `useMemo` only when the computation is measurably expensive (sorting/filtering large lists) or its identity feeds a dependency array. Memoizing string concatenation is noise — delete it.
- To reset state when an identity prop changes, change the component's `key` from the parent. Do not write a `useEffect` that watches the prop and calls a setter.
- "Adjust state during render" pattern (set state conditionally before returning, guarded by a comparison) is allowed only for the rare reset-on-prop-change case that `key` can't express; comment why.
## useEffect rules
- An effect exists to synchronize with something outside React: subscriptions, DOM APIs, timers, analytics, imperative widgets. If an effect only calls `setState` from other state/props, it is a bug — derive instead.
- Event logic belongs in event handlers. Never set a `submitted` flag in state and "react" to it in an effect; do the work in `handleSubmit`.
- Dependency arrays are never lied to. Do not omit a dependency to "run once" — restructure instead:
- stable callback identity → `useCallback` (or an effect-event-style ref) at the source;
- object/array deps recreated each render → destructure to primitives or move the literal inside the effect;
- genuinely mount-only browser setup → empty array is fine, but the effect body must not read changing props/state.
- Every subscription/listener/timer effect returns a cleanup function. An `addEventListener` without a matching cleanup is a defect, not a style issue.
- Fetching in effects requires an `AbortController` (or ignore-stale flag) in cleanup. If the project has React Query/SWR, fetching in `useEffect` is banned outright.
## Custom hook extraction thresholds
Extract `useX()` into its own file only when ALL of these hold:
1. The logic spans 2+ hooks working together (e.g. state + effect + ref), not a single `useState`.
2. It is used by 2+ components, or it isolates one nameable external concern (`useMediaQuery`, `useDebouncedValue`, `useLocalStorage`).
3. It returns a small, named API — `{ value, setValue }` or `[state, actions]` — never the raw setter soup of its internals.
Do not extract: one-component logic just to shorten the component, hooks that take 6+ parameters (split them), or hooks that render JSX (that's a component).
## Misc
- Functional updates (`setCount(c => c + 1)`) whenever next state depends on previous state, and always inside async callbacks.
- `useRef` for values that change without needing a re-render (latest callback, scroll position); never read/write refs during render.
- Hooks are called unconditionally at the top of the component — early returns come after the last hook.