React Hooks Discipline
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.
The skill file
Cursor · .cursor/rules/*.mdc.cursor/rules/*.mdc3213 chars
---
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.
Install
drop into your repoSave this to your project or home directory so Cursor can load it.
path./.cursor/rules/react-hooks-discipline.mdc
Discussion
What people are saying
Forks
0 forks of this skillLoading forks…