Last active
September 22, 2025 10:03
-
-
Save byigitt/6bceb4649b542e67af01533bdf970f0b to your computer and use it in GitHub Desktop.
"You Might Not Need an Effect" article from React.dev - cursorrule version
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| --- | |
| description: Prefer render-time derivations, event handlers, keys, and external-store subscriptions over useEffect. Only use Effects to sync with external systems. Include refactor suggestions. | |
| globs: | |
| --- | |
| # React “You Might Not Need an Effect” — Project Rule | |
| ## Golden Rule | |
| Before writing or keeping a `useEffect`, ask: **Is there an external system that must be synchronized (DOM outside React, timers, subscriptions, storage, network, non-React widget)?** | |
| - **If NO** → remove/avoid the Effect and prefer render-time code or event handlers. | |
| - **If YES** → keep the Effect (or use `useSyncExternalStore`) and make it idempotent with proper cleanup. | |
| --- | |
| ## 1) Derived values → compute during render (not in Effects) | |
| - ❌ **Anti-pattern:** Effect that **sets state** with a value computed only from props/state. | |
| - ✅ **Do:** Compute directly in render. | |
| ```tsx | |
| // BAD | |
| const [fullName, setFullName] = useState(""); | |
| useEffect(() => setFullName(`${first} ${last}`), [first, last]); | |
| // GOOD | |
| const fullName = `${first} ${last}`; | |
| ```` | |
| * **Refactor heuristic:** If an Effect only calls `setX(derive(props,state))`, inline the derivation and delete the Effect & state. | |
| --- | |
| ## 2) Expensive derivations → `useMemo`, not Effects | |
| * ❌ Effect that recomputes filtered/derived arrays and stores them in state. | |
| * ✅ Use `useMemo` for pure, expensive work keyed by its true dependencies. | |
| ```tsx | |
| const visibleTodos = useMemo( | |
| () => getFilteredTodos(todos, filter), | |
| [todos, filter] | |
| ); | |
| ``` | |
| * **Heuristic:** If derivation depends purely on inputs and affects rendering only, prefer `useMemo`. | |
| --- | |
| ## 3) Resetting state on identity change → use `key` | |
| * ❌ Effect that clears local state when a prop like `userId` changes. | |
| * ✅ Split and **key** the inner component so all nested state resets automatically. | |
| ```tsx | |
| export function ProfilePage({ userId }: { userId: string }) { | |
| return <Profile key={userId} userId={userId} />; | |
| } | |
| ``` | |
| --- | |
| ## 4) Adjusting *some* state on prop change → prefer render logic | |
| * Try to **store stable identifiers** (e.g., `selectedId`) and derive the selected entity during render: | |
| ```tsx | |
| const selection = items.find(i => i.id === selectedId) ?? null; | |
| ``` | |
| * If truly needed, you may set state during render **in the same component** with a guard: | |
| ```tsx | |
| const [prevItems, setPrevItems] = useState(items); | |
| if (items !== prevItems) { setPrevItems(items); setSelection(null); } | |
| ``` | |
| (Only update **this** component’s state; include guards to avoid loops.) | |
| --- | |
| ## 5) User actions → event handlers, not Effects | |
| * ❌ Posting `/api/buy` or showing notifications inside an Effect that keys off state flags. | |
| * ✅ Put user-driven work in the handler so it runs exactly once per interaction. | |
| ```tsx | |
| function handleSubmit(e) { | |
| e.preventDefault(); | |
| post("/api/register", { firstName, lastName }); | |
| } | |
| ``` | |
| --- | |
| ## 6) Multi-step logic → do it in one handler, avoid chains of Effects | |
| * ❌ Cascading Effects that update one piece of state to trigger the next. | |
| * ✅ Compute next states together inside the event handler; set all new state in one go. | |
| --- | |
| ## 7) One-time app init | |
| * Top-level app bootstrap should be resilient to remounts. If something must run once per app load, | |
| guard it with a module variable or put it in the entry point—not as a naive `useEffect(() => {...}, [])` | |
| that can mount twice in dev. | |
| --- | |
| ## 8) Notify parent about internal changes → do it in the same event | |
| * ❌ Effect that calls `onChange(isOn)` after internal state changes. | |
| * ✅ Call parent callback **together** with local `setState` in the originating event. | |
| ```tsx | |
| function updateToggle(next) { setIsOn(next); onChange(next); } | |
| ``` | |
| --- | |
| ## 9) Data flow direction → parent fetches, children consume | |
| * ❌ Child fetches data then pushes up via Effect (`onFetched`). | |
| * ✅ Parent fetches (or uses a hook) and passes `data` down as props. | |
| --- | |
| ## 10) Subscriptions to external stores → `useSyncExternalStore` | |
| * Prefer: | |
| ```tsx | |
| function subscribe(cb: () => void) { addEventListener('online', cb); return () => removeEventListener('online', cb); } | |
| function useOnlineStatus() { | |
| return useSyncExternalStore(subscribe, () => navigator.onLine, () => true); | |
| } | |
| ``` | |
| * Use Effects for subscriptions only if `useSyncExternalStore` is inapplicable; always clean up. | |
| --- | |
| ## 11) Fetching with Effects → include cleanup to avoid races | |
| * Keep search/results “in sync” with `query`, `page`, etc., but cancel/ignore stale responses: | |
| ```tsx | |
| useEffect(() => { | |
| let ignore = false; | |
| fetchResults(query, page).then(json => { if (!ignore) setResults(json); }); | |
| return () => { ignore = true; }; | |
| }, [query, page]); | |
| ``` | |
| * Consider a `useData(url)` hook to centralize patterns (loading, errors, caching), or adopt your framework’s data fetching. | |
| --- | |
| ## 12) Quick checklist (auto-review) | |
| When you see a `useEffect`, verify: | |
| 1. **External system?** If no, remove it. | |
| 2. **Only sets derived state?** Inline the derivation. | |
| 3. **User interaction?** Move to event handler. | |
| 4. **Reset on identity?** Use `key`. | |
| 5. **Subscriptions?** Prefer `useSyncExternalStore`. | |
| 6. **Fetching?** Add cleanup; avoid races; cache if needed. | |
| 7. **Effect chains?** Collapse into single handler update. | |
| --- | |
| ## 13) Transformation hints (for the assistant) | |
| * Replace: | |
| * `useEffect(() => setX(f(props, state)), [deps])` → `const x = f(props, state)` | |
| * State + Effect memoization → `useMemo` | |
| * Prop-change resets → `key` | |
| * Parent notification in Effect → call `onChange` in the same event | |
| * External mutable source → `useSyncExternalStore` | |
| * Fetch without cleanup → add cancellation/ignore flag (or AbortController) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment