no-use-effect
No useEffect
Treat raw useEffect as a last resort, not a default tool. Most component logic belongs in
render, event handlers, framework data APIs, or narrower React primitives.
This skill is based primarily on the React docs (You Might Not Need an Effect,
Synchronizing with Effects, Removing Effect Dependencies), with supporting guidance from
Dan Abramov, Kent C. Dodds, TkDodo, and team practices that ban or heavily restrict raw
useEffect.
Core Rule
Before adding an Effect, answer this question:
"What external system am I synchronizing with, and why can't this be done during render, in an event handler, or with a more specific primitive?"
If you cannot name the external system, do not write the Effect.
External systems include:
- browser APIs and DOM subscriptions
- timers, sockets, observers, media APIs
- third-party widgets and imperative libraries
- network synchronization that truly happens because the component is visible
Not external systems:
- derived values from props or state
- event-driven actions like submit, save, buy, delete, notify
- resetting local state because props changed
- chaining state updates to trigger more state updates
- passing data upward just because a child rendered
Replacement Patterns
1. Derive during render
Do not mirror props or state into more state.
// Bad
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
// Good
const fullName = `${firstName} ${lastName}`;
Use this for filtered lists, labels, counts, booleans, selected objects, and other values that can be computed from current inputs.
2. Memoize expensive pure work
If the calculation is slow, cache it with useMemo. Do not use an Effect that immediately
calls setState with a computed value.
const visibleTodos = useMemo(() => getVisibleTodos(todos, filter), [todos, filter]);
First ask whether the work is actually expensive. If not, calculate it directly during render.
3. Put user actions in event handlers
If the code runs because the user clicked, submitted, selected, dragged, or typed, keep it in the corresponding handler.
// Bad
useEffect(() => {
if (shouldSubmit) {
void saveForm(form);
}
}, [shouldSubmit, form]);
// Good
async function handleSubmit() {
await saveForm(form);
}
Never build "set flag -> Effect notices flag -> Effect does the real work" flows unless the flag is synchronizing with a real external system.
4. Reset identity with key when you want a fresh instance
If a component should become a brand-new instance when some identity changes, give it a
different key. React will unmount the old subtree and mount a new one, which resets all local
state below that point without an Effect.
// Bad
function Profile({ userId }: { userId: string }) {
const [comment, setComment] = useState("");
useEffect(() => {
setComment("");
}, [userId]);
return <input value={comment} onChange={(e) => setComment(e.target.value)} />;
}
// Good
function ProfilePage({ userId }: { userId: string }) {
return <Profile key={userId} userId={userId} />;
}
function Profile({ userId }: { userId: string }) {
const [comment, setComment] = useState("");
return <input value={comment} onChange={(e) => setComment(e.target.value)} />;
}
Use this when the identity really changed: user, document, chat room, wizard step, or form instance. Do not use it when you only want to tweak one field, because remounting resets the entire subtree.
5. Lift state or control the component
If an Effect exists only to notify a parent after local state changed, the data model is usually wrong. Prefer lifting state up or updating both sides in the same event.
// Bad
function Toggle({ onChange }: { onChange: (next: boolean) => void }) {
const [isOn, setIsOn] = useState(false);
useEffect(() => {
onChange(isOn);
}, [isOn, onChange]);
return <button onClick={() => setIsOn(!isOn)}>{isOn ? "On" : "Off"}</button>;
}
// Good
function Toggle({ isOn, onChange }: { isOn: boolean; onChange: (next: boolean) => void }) {
return <button onClick={() => onChange(!isOn)}>{isOn ? "On" : "Off"}</button>;
}
6. Use useSyncExternalStore for external subscriptions
Use useSyncExternalStore when the source of truth lives outside React and changes over time.
This is React's built-in primitive for subscribing to external stores.
Good fits:
- browser APIs with a current value plus subscription events
- third-party or app-level stores outside React state
- reusable hooks like
useOnlineStatus()oruseMediaQuery()
The shape is:
const value = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
Where:
subscribetells React how to listen for changes and return cleanupgetSnapshotreads the current value from the external storegetServerSnapshotprovides the initial server and hydration value when SSR is involved
// Bad
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function updateStatus() {
setIsOnline(navigator.onLine);
}
updateStatus();
window.addEventListener("online", updateStatus);
window.addEventListener("offline", updateStatus);
return () => {
window.removeEventListener("online", updateStatus);
window.removeEventListener("offline", updateStatus);
};
}, []);
return isOnline;
}
// Good
const isOnline = useSyncExternalStore(subscribe, () => navigator.onLine, () => true);
Prefer this over hand-rolled subscription Effects in components.
Important caveats from the React docs:
getSnapshotmust return a stable cached value unless the store actually changed- if
subscribechanges identity every render, React will resubscribe every render - if you support SSR,
getServerSnapshotmust match between server render and hydration
Do not use this for:
- normal component state that should live in
useState,useReducer, or context - derived values you can calculate during render
- user actions handled by event handlers
- one-time fetching
- general side effects that are not subscriptions
7. Fetch through the framework or a data library
Avoid raw fetch-in-Effect code in app components when a framework loader or query library is available.
Preferred order:
- framework data APIs (
Next.js,React Router, Remix, server components, route loaders) - data libraries (
TanStack Query,SWR) - a focused custom hook
- raw
useEffectonly if none of the above fit
If you must fetch in an Effect, handle cleanup so stale responses are ignored.
// Bad
useEffect(() => {
void fetchResults(query).then(setResults);
}, [query]);
// Good
useEffect(() => {
let ignore = false;
void fetchResults(query).then((next) => {
if (!ignore) {
setResults(next);
}
});
return () => {
ignore = true;
};
}, [query]);
8. Encapsulate unavoidable imperative synchronization
Sometimes an Effect is correct: sockets, observers, widgets, media controls, focus management, layout measurement, or DOM APIs that must stay in sync with rendered output.
When that happens:
- keep the Effect tiny and specific
- name the external system in a comment or hook name
- include full cleanup
- prefer a custom hook over open-coded Effect logic in many components
- consider
useLayoutEffectonly for pre-paint measurement or visual correctness
// Bad
function ChatRoom({ roomId }: { roomId: string }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
}, [roomId]);
}
// Good
function useChatConnection(roomId: string) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
}
If your team bans raw useEffect, put these escape hatches behind approved hooks instead of
sprinkling Effects through feature code.
Anti-Patterns
Treat these as red flags during authoring or review:
useEffect(() => setX(...), [...])for derived state- Effect only exists to call a function after a button click
- effect chains where one state update triggers another Effect and another render
- dependency arrays fought with
eslint-disable, refs-as-flags, or intentionally stale closures - object or function dependencies recreated every render without need
- child component fetching data and pushing it upward in an Effect
- mount-only initialization that should live in module scope, entrypoints, or framework boot code
Review Checklist
Before accepting a new Effect, verify all of these:
- The code synchronizes with a real external system
- Derived render data, memoization, events,
key, lifted state, anduseSyncExternalStorewere all considered first - The dependency list is honest and complete
- Setup and cleanup are symmetrical
- The Effect is idempotent under Strict Mode re-mounting
- Data fetching is not bypassing an existing framework or query abstraction
- The logic is small enough that a custom hook or helper name explains its purpose
Team Policy
For teams trying to ban or heavily restrict raw useEffect, use this policy:
- Do not introduce
useEffectfor derived state, events, or resets. - Prefer render-time derivation, event handlers,
key,useMemo, anduseSyncExternalStore. - Route data loading through the framework or an approved query library.
- If an Effect is still necessary, hide it behind a narrowly named custom hook or document the external system and cleanup explicitly in review.
- Never silence
react-hooks/exhaustive-depsto "make it work".
References
For source material and further reading, see references/sources.md.