Finding React Effects You Don't Need
Use eslint-plugin-react-you-might-not-need-an-effect to spot unnecessary React effects and refactor derived state, event handlers, data flow, and prop-based resets.
eslint-plugin-react-you-might-not-need-an-effect is an ESLint and Oxlint plugin that flags useEffect calls that React’s documentation calls unnecessary, such as effects that derive state, relay events, or pass data up to a parent.
You’ve read You Might Not Need an Effect, nodded along, and then opened a codebase with four hundred useEffect calls and no practical way to tell which ones are the problem.
This article shows how to install the plugin, what it reports on a realistic component, how to refactor the four most common findings, and when a warning should be suppressed rather than fixed. For a refresher on the hook itself, see this guide to React’s useEffect hook.
Key Takeaways
- eslint-plugin-react-you-might-not-need-an-effect ships nine rules and two flat-config presets:
recommendedreports every rule as a warning, andstrictreports every rule as an error. Legacy .eslintrc projects get matchinglegacy-recommendedandlegacy-strictpresets. - Work out values that depend only on current props and state during render, rather than copying them into state with an effect.
- To reset all of a component’s state when a prop such as a customer ID changes, render the component with that prop as its
key. - Effects that keep React in step with an outside system, such as a timer, a DOM API, a non-React widget or a network connection, are the job
useEffectexists for, so keep them. - Suppress a warning with an
eslint-disable-next-linecomment that names the rule and gives a reason after--.
What Makes a React Effect Unnecessary?
Most unnecessary effects come in two shapes: an effect that recomputes a value and stores it in state, or an effect that watches one piece of state and updates another to match. In both cases React commits a render with the old value, runs the effect, and then renders again with the corrected one.
The extra render caused by an unnecessary effect is visible to users. Session replays of effect-heavy components can show a blank filtered list or a placeholder total for a frame before the second render fills it in. The fix is almost always to remove the state rather than tune the effect.
Install and Run the Plugin
The plugin installs as a dev dependency and plugs into ESLint flat config through its recommended preset. If you already manage several ESLint plugins, it sits alongside them without conflict.
npm install --save-dev eslint-plugin-react-you-might-not-need-an-effect
// eslint.config.js
import reactYouMightNotNeedAnEffect from "eslint-plugin-react-you-might-not-need-an-effect";
import globals from "globals";
export default [
reactYouMightNotNeedAnEffect.configs.recommended,
{
languageOptions: {
globals: { ...globals.browser },
parserOptions: { ecmaFeatures: { jsx: true } },
},
},
];
The examples target version 1.0.2, released on 20 August 2026 according to the plugin’s CHANGELOG. The same changelog records that no-pass-ref-to-parent and no-empty-effect were removed before 1.0, so delete them from older configs. Oxlint users load the plugin through Oxlint’s jsPlugins field. Rule IDs use the kebab-case react-you-might-not-need-an-effect/ prefix in Oxlint, in legacy configs, and in flat config when you use the presets, because the presets register the plugin under its package name. The camelCase reactYouMightNotNeedAnEffect/ prefix only applies if you register the plugin yourself under that key, as the README’s single-rule example does.
Here is a component with four effects, each written to match one rule’s documented pattern:
import { useEffect, useState } from "react";
import { postOrder, useCustomerHistory } from "./api";
export function OrderPanel({ customerId, items, onLoaded }) {
const [total, setTotal] = useState(0);
const [submitted, setSubmitted] = useState(false);
const [note, setNote] = useState("");
const history = useCustomerHistory(customerId);
// Effect 1
useEffect(() => {
setTotal(items.reduce((sum, i) => sum + i.price * i.qty, 0));
}, [items]);
// Effect 2
useEffect(() => {
if (submitted) postOrder(customerId, items, note);
}, [submitted, customerId, items, note]);
// Effect 3
useEffect(() => {
onLoaded(history);
}, [history, onLoaded]);
// Effect 4
useEffect(() => {
setNote("");
setSubmitted(false);
}, [customerId]);
return (
<form onSubmit={(e) => { e.preventDefault(); setSubmitted(true); }}>
<textarea value={note} onChange={(e) => setNote(e.target.value)} />
<p>Total: {total}</p>
<button type="submit">Place order</button>
</form>
);
}
Run npx eslint src/OrderPanel.jsx. Under recommended, findings appear as warnings with the rule ID at the end of each line. Each effect maps to the rule it triggers:
| Effect | Pattern | Rule |
|---|---|---|
| 1 | State computed from props | no-derived-state |
| 2 | State flag used as an event trigger | no-event-handler |
| 3 | Fetched data handed to a parent callback | no-pass-data-to-parent |
| 4 | State cleared when a prop changes | no-adjust-state-on-prop-change (one warning per setter) |
Effect 4 is the case key solves. no-reset-all-state-on-prop-change reports the same shape in simpler components, but in a component like this one, which reads its state while rendering, version 1.0.2 reports each reset under no-adjust-state-on-prop-change instead.
The full rule set, with the replacement for each:
| Rule | Replace the effect with |
|---|---|
no-derived-state | A value computed during render |
no-chain-state-updates | One consolidated state update |
no-event-handler | Code in the event handler |
no-adjust-state-on-prop-change | Restructured state or a render-time calculation |
no-reset-all-state-on-prop-change | The prop passed as key |
no-pass-live-state-to-parent | Lifted state, or a returned value from a hook |
no-pass-data-to-parent | Fetching in the parent, result passed as props |
no-external-store-subscription | useSyncExternalStore |
no-initialize-state | An initial value in useState() |
The plugin traces call chains and dependency arrays, so it adds lint work. ESLint’s rule profiling (TIMING=1) shows what that costs in your own repository. The README also suggests enabling react-hooks/exhaustive-deps, because the plugin assumes your dependency arrays are correct, and typescript-eslint/no-floating-promises, which helps it recognise calls to async functions.
Derived State: Compute It During Render
When a value can be worked out from the props and state you already have, compute it during render. Don’t keep a copy in state and sync it with an effect. React’s docs cover this under updating state based on props or state.
// Before
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(items.reduce((sum, i) => sum + i.price * i.qty, 0));
}, [items]);
// After
const total = items.reduce((sum, i) => sum + i.price * i.qty, 0);
The total is now correct on the first render. If the calculation turns out to be expensive, wrap it in useMemo. Don’t put it back into state.
Event Handler Work: Move It Into the Handler
An effect that waits for a state flag set in a submit or click handler is an event handler in disguise. Run that logic in the handler itself. The rationale is laid out in Separating Events from Effects.
// Before
const [submitted, setSubmitted] = useState(false);
useEffect(() => {
if (submitted) postOrder(customerId, items, note);
}, [submitted, customerId, items, note]);
// After
function handleSubmit(e) {
e.preventDefault();
postOrder(customerId, items, note);
}
The “before” version has a real bug: editing the note after submitting posts the order again. Since 0.10.1, no-event-handler also flags props used this way, and since 0.10.2 it reports at the usage site rather than the declaration.
Data Fetching and Passing Data Up
A child that fetches data and hands it to its parent through an effect should give that job to the parent, the framework’s data loading, or a data-fetching library, and receive the result as props. React describes the same inversion in passing data to the parent.
// Before: child fetches, then pushes up
const history = useCustomerHistory(customerId);
useEffect(() => {
onLoaded(history);
}, [history, onLoaded]);
// After: parent owns the data
function OrderPage({ customerId, items }) {
const history = useCustomerHistory(customerId); // loader, library, or parent effect
return <OrderPanel customerId={customerId} items={items} history={history} />;
}
When data flows one way, it’s clear which component owns it, and OrderPanel no longer needs the onLoaded callback prop just to report what it loaded.
Resetting State on Prop Change: Use key
To reset all of a component’s state when a prop such as a customer ID changes, render the component with that prop as its key so React mounts a fresh instance. The pattern is documented under resetting all state when a prop changes.
// Before: Effect 4 inside OrderPanel
useEffect(() => {
setNote("");
setSubmitted(false);
}, [customerId]);
// After: delete the effect; in the parent
<OrderPanel key={customerId} customerId={customerId} items={items} history={history} />
no-adjust-state-on-prop-change is aimed at the partial version of this, where only some state changes in response to a prop, although it also reports Effect 4, as the lint run above shows. When only part of the state should reset, the fix is usually to restructure the state or calculate the value during render.
False Positives and Effects That Stay
Effects that connect a component to something React doesn’t manage (timers, DOM APIs, non-React widgets, network connections) are the job useEffect was built for, so leave them in place. Most of them need a cleanup function, as Synchronizing with Effects explains. If a DOM measurement causes a visible flicker, the choice between useEffect and useLayoutEffect matters more than whether to keep the effect.
Subscriptions to an external store are the exception to keeping synchronization effects. They belong in useSyncExternalStore, which reads a snapshot during render instead of copying it into state from an effect. no-external-store-subscription flags the effect-based version.
Static analysis of effects has edge cases. React’s own set-state-in-effect rule warns when an effect calls a state setter synchronously, and a React issue on set-state-in-effect strictness disputes how strictly it treats mount flags and client-detection patterns. Treat warnings on those shapes as a prompt to check the code, not as a verdict either way. Running both plugins is reasonable: the React rule covers setState in effects, and this plugin adds parent callbacks, chained updates and store subscriptions.
Before suppressing a warning, ask:
- Does the effect talk to a system outside React?
- Does it need cleanup when dependencies change or the component unmounts?
- Would moving it to render or a handler change behavior users can see?
If the answers justify keeping it, disable the rule on that line and use ESLint’s -- description syntax to record why:
// eslint-disable-next-line react-you-might-not-need-an-effect/no-initialize-state -- client-only value, avoids hydration mismatch
This prefix matches the presets, Oxlint and legacy configs. Use reactYouMightNotNeedAnEffect/ only if you registered the plugin under that key yourself.
Rolling It Out
Roll out eslint-plugin-react-you-might-not-need-an-effect on recommended first, so the findings show up as warnings and CI keeps passing. Work through them one component at a time, refactoring or annotating each, and switch to strict once the warning count reaches zero, so that new unnecessary effects fail the build.
FAQs
Can I use eslint-plugin-react-you-might-not-need-an-effect with a legacy .eslintrc config?
Yes. Legacy .eslintrc projects extend plugin:react-you-might-not-need-an-effect/legacy-recommended to report every rule as a warning, or plugin:react-you-might-not-need-an-effect/legacy-strict to report them as errors. In legacy configs, rule IDs and disable comments use the kebab-case react-you-might-not-need-an-effect/ prefix, the same prefix the flat-config presets use.
When should I wrap a derived value in useMemo instead of computing it on every render?
Use useMemo only when the calculation is measurably slow. React's documentation suggests timing it with console.time and memoizing if the total comes to around 1ms or more. useMemo caches the result between renders while keeping it out of state, so it stays correct on the first render. React Compiler can memoize many calculations automatically, which makes manual useMemo calls for derived values less often necessary.
Does changing a component's key also reset the state of its children?
Yes. When the key changes, React unmounts the component and its entire subtree, then mounts fresh instances. Every child loses its local state, the DOM nodes are recreated, and all effects run their cleanup and then run again. If only some state should reset while child state survives, adjust that state directly or restructure it rather than using key.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k