Skip to content

Instantly share code, notes, and snippets.

@byigitt
Last active September 22, 2025 10:03
Show Gist options
  • Select an option

  • Save byigitt/6bceb4649b542e67af01533bdf970f0b to your computer and use it in GitHub Desktop.

Select an option

Save byigitt/6bceb4649b542e67af01533bdf970f0b to your computer and use it in GitHub Desktop.
"You Might Not Need an Effect" article from React.dev - cursorrule version
---
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