eslint-plugin-react-you-might-not-need-an-effect: The React Docs Page That Learned to Read Your Code
If you have spent any meaningful time building React applications, you have probably encountered the official React documentation page titled "You Might Not Need an Effect." It is one of the most frequently referenced pages in the React ecosystem, packed with practical advice about when useEffect is the wrong tool for the job. The problem is that documentation does not enforce itself. Developers read it, nod along, and then write the exact anti-patterns it warns against three hours later. eslint-plugin-react-you-might-not-need-an-effect takes that entire documentation page and turns it into a set of nine ESLint rules that automatically flag unnecessary effects in your codebase.
Created by Nick van Dyke, this plugin has grown from zero to over 224,000 weekly downloads in under a year, filling a gap that even React's own official linting tools leave open.
Why Your Effects Probably Need a Second Opinion
The core insight behind this plugin is that most unnecessary useEffect calls fall into a handful of well-documented categories. You might be storing derived state in an effect when it could be a simple variable. You might be chaining state updates across multiple effects when the logic belongs in a single event handler. You might be using an effect to pass data to a parent component when the data flow should be inverted.
React's official eslint-plugin-react-hooks covers the rules of hooks and exhaustive dependencies, but it does not analyze the semantic purpose of your effects. The newer set-state-in-effect rule broadly flags synchronous setState calls inside effects, but it cannot distinguish between a legitimate asynchronous initialization and a derived-state anti-pattern. That is exactly where eslint-plugin-react-you-might-not-need-an-effect shines: it understands the context of what your effect is doing and why it is probably wrong.
Nine Rules, Nine Rescued Codebases
The plugin ships with nine rules, each corresponding to a specific anti-pattern. Here is the full lineup:
- no-derived-state -- State that can be computed from props or other state
- no-chain-state-updates -- Effects that update state in response to other state changes
- no-event-handler -- Effects acting as event handlers
- no-adjust-state-on-prop-change -- Effects adjusting state when a prop changes
- no-reset-all-state-on-prop-change -- Effects resetting all state when a prop changes
- no-pass-live-state-to-parent -- Effects passing live state up to parent components
- no-pass-data-to-parent -- Effects passing fetched data to parent components
- no-initialize-state -- Effects initializing state with constant values
- no-empty-effect -- Empty effects that do nothing at all
Getting Started
Install the plugin as a dev dependency:
npm install --save-dev eslint-plugin-react-you-might-not-need-an-effect
or
yarn add -D eslint-plugin-react-you-might-not-need-an-effect
Zero-Config Presets
The fastest way to get going is with one of the two built-in presets. If you are using ESLint's flat config format:
// eslint.config.js
import reactYouMightNotNeedAnEffect from "eslint-plugin-react-you-might-not-need-an-effect";
export default [
reactYouMightNotNeedAnEffect.configs.recommended, // all rules as warnings
];
The recommended preset enables every rule at the warn level, giving your team visibility without blocking CI. When you are ready to enforce, switch to strict:
// eslint.config.js
import reactYouMightNotNeedAnEffect from "eslint-plugin-react-you-might-not-need-an-effect";
export default [
reactYouMightNotNeedAnEffect.configs.strict, // all rules as errors
];
For projects still using legacy .eslintrc configuration:
{
"extends": [
"plugin:react-you-might-not-need-an-effect/legacy-recommended"
]
}
Catching Derived State Before It Catches You
The no-derived-state rule is arguably the most impactful. It catches the classic pattern where developers store a value in state via useEffect when that value is entirely computable from existing props or state:
// This triggers no-derived-state
function UserProfile({ firstName, lastName }: { firstName: string; lastName: string }) {
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
return <h1>{fullName}</h1>;
}
The fix is almost always simpler than the original code:
function UserProfile({ firstName, lastName }: { firstName: string; lastName: string }) {
const fullName = `${firstName} ${lastName}`;
return <h1>{fullName}</h1>;
}
No extra state, no extra render cycle, no effect cleanup to worry about. If the computation is expensive, wrap it in useMemo. The plugin is smart enough to detect derived state even when it originates from external sources like useQuery -- as long as the setter is only called in one place, it recognizes the pattern.
Breaking Effect Chains
The no-chain-state-updates rule targets one of the most insidious performance patterns: cascading effects that trigger each other through state changes. Each effect in the chain causes a new render, and the result is a waterfall of unnecessary work:
// This triggers no-chain-state-updates
function GameBoard() {
const [score, setScore] = useState(0);
const [level, setLevel] = useState(1);
const [message, setMessage] = useState("");
useEffect(() => {
if (score > 100) {
setLevel(2);
}
}, [score]);
useEffect(() => {
if (level === 2) {
setMessage("Welcome to level 2!");
}
}, [level]);
return <div>{message}</div>;
}
The fix consolidates the logic so it does not bounce through multiple render cycles:
function GameBoard() {
const [score, setScore] = useState(0);
const level = score > 100 ? 2 : 1;
const message = level === 2 ? "Welcome to level 2!" : "";
return <div>{message}</div>;
}
Effects Are Not Event Handlers
The no-event-handler rule catches a subtle but common mistake: using an effect to run side effects that logically belong in an event handler. The telltale sign is an effect that watches a piece of state and does something when it changes, instead of doing that something at the point where the state was set:
// This triggers no-event-handler
function ShoppingCart({ product }: { product: Product }) {
const [isInCart, setIsInCart] = useState(false);
useEffect(() => {
if (isInCart) {
showNotification(`Added ${product.name} to cart!`);
}
}, [isInCart, product.name]);
function handleAddToCart() {
setIsInCart(true);
}
return <button onClick={handleAddToCart}>Add to Cart</button>;
}
The notification should live in the event handler, not in an effect that reacts to state:
function ShoppingCart({ product }: { product: Product }) {
const [isInCart, setIsInCart] = useState(false);
function handleAddToCart() {
setIsInCart(true);
showNotification(`Added ${product.name} to cart!`);
}
return <button onClick={handleAddToCart}>Add to Cart</button>;
}
The Key Pattern for State Resets
When all of a component's state needs to reset in response to a prop change, developers often reach for an effect. The no-reset-all-state-on-prop-change rule catches this and suggests the key prop pattern instead:
// This triggers no-reset-all-state-on-prop-change
function ChatRoom({ roomId }: { roomId: string }) {
const [messages, setMessages] = useState<string[]>([]);
const [draft, setDraft] = useState("");
useEffect(() => {
setMessages([]);
setDraft("");
}, [roomId]);
return <div>{/* chat UI */}</div>;
}
The key prop tells React to treat this as an entirely new component instance:
// In the parent component
<ChatRoom roomId={roomId} key={roomId} />
No effect needed. React handles the full unmount and remount, resetting all state automatically.
Fine-Tuning Individual Rules
If the strict preset is too aggressive for your team, you can configure rules individually. This is useful during gradual adoption:
// eslint.config.js
import reactYouMightNotNeedAnEffect from "eslint-plugin-react-you-might-not-need-an-effect";
export default [
{
plugins: {
"react-you-might-not-need-an-effect": reactYouMightNotNeedAnEffect,
},
rules: {
"react-you-might-not-need-an-effect/no-derived-state": "error",
"react-you-might-not-need-an-effect/no-chain-state-updates": "error",
"react-you-might-not-need-an-effect/no-event-handler": "warn",
"react-you-might-not-need-an-effect/no-empty-effect": "error",
"react-you-might-not-need-an-effect/no-initialize-state": "warn",
},
},
];
The plugin also recommends pairing with react-hooks/exhaustive-deps and typescript-eslint/no-floating-promises as companion rules. The former ensures your effects have correct dependency arrays (which the plugin assumes), and the latter helps the plugin accurately infer async function calls.
Wrapping Up
eslint-plugin-react-you-might-not-need-an-effect does something remarkably practical: it takes one of the most useful pages in the React documentation and makes it enforceable at the linting level. Instead of relying on code review to catch derived state, chained effects, and misplaced event logic, you get immediate feedback in your editor as you type.
With nine targeted rules, zero-config presets, support for both flat config and legacy .eslintrc, and deep analysis that goes well beyond "you called setState in an effect," this plugin earns its place in any serious React project's ESLint configuration. The best effect is the one you never had to write.