Instant PR Rejection If You Use useEffect Like This

The useEffect hook is one of the most commonly used hooks in React. However, it is frequently misunderstood and misused.
While reviewing hundreds of pull requests at my GenAI startup job, I noticed recurring pitfalls in how useEffect is implemented, along with opportunities to eliminate unnecessary uses of it.
I’ve made these mistakes myself, and I’ve also reviewed countless PRs where useEffect was used incorrectly.
According to the official documentation, the purpose of the useEffect hook is to handle side effects in function components. Side effects include tasks that don’t directly relate to rendering, such as:
Fetching data from an API (if no third-party library is used)
Subscribing to events (e.g., WebSocket, DOM events)
Setting up timers or intervals (e.g., setTimeout or setInterval)
Now that we’ve refreshed our understanding of useEffect, let’s look at some common patterns where the useEffect hook is not necessary.
1) Eliminate useEffect via derived value
One common pattern for eliminating a useEffect is by using derived values directly in the component, rather than relying on state that’s updated inside a useEffect:
// BEFORE: Using useEffect to derive state
function BeforeExample({ selectedId }) {
const [selectedItem, setSelectedItem] = useState(null);
// ❌ Unnecessary useEffect
useEffect(() => {
const item = items.find(item => item.id === selectedId);
setSelectedItem(item);
}, [selectedId]);
return <div>{selectedItem?.name}</div>;
}
// AFTER: Computing value directly
function AfterExample({ selectedId }) {
// ✅ Compute derived value directly
const selectedItem = items.find(item => item.id === selectedId);
return <div>{selectedItem?.name}</div>;
}
In the example above, you can see that in the second version we completely eliminated the useEffect (and the state) by computing the necessary value directly within the component.
2) Eliminate useEffect via the key property.
Another effective approach is using the key prop to reset a component’s internal state when its props change. This eliminates the need for a useEffect to synchronize state with props.
🛑 Don’t do this:
function EditForm({ itemData }) {
const [formValues, setFormValues] = useState(itemData);
// ❌ useEffect to update form values when itemData changes
useEffect(() => {
setFormValues(itemData);
}, [itemData]);
const handleChange = (e) => {
setFormValues({
...formValues,
[e.target.name]: e.target.value
});
};
return (
<form>
<input
name="title"
value={formValues.title || ''}
onChange={handleChange}
/>
<textarea
name="description"
value={formValues.description || ''}
onChange={handleChange}
/>
<button type="submit">Save</button>
</form>
);
}
In this version, useEffect
is used to sync internal state (formValues
) with the incoming itemData
prop. While this works, it introduces unnecessary complexity.
✅ Do this instead:
// Parent component
function FormContainer({ itemData }) {
// ✅ Key forces the form to be recreated with new initial values
return <EditForm key={itemData.id} itemData={itemData} />;
}
function EditForm({ itemData }) {
// ✅ Initialize state with prop values - this happens once when component mounts
const [formValues, setFormValues] = useState(itemData);
// ✅ No useEffect needed - the component will remount with fresh props when key changes
const handleChange = (e) => {
setFormValues({
...formValues,
[e.target.name]: e.target.value,
});
};
return (
<form>
<input
name="title"
value={formValues.title || ''}
onChange={handleChange}
/>
<textarea
name="description"
value={formValues.description || ''}
onChange={handleChange}
/>
<button type="submit">Save</button>
</form>
);
}
By passing a key prop to EditForm, React will remount the component whenever itemData.id
changes. This causes the component to reinitialize its state with the new itemData
— removing the need for an extra useEffect
and making the logic easier to reason about.
There are three common pitfalls with the useEffect hook.
1) Forgetting a Dependency
import React, { useState, useEffect } from 'react';
function PitfallExamples() {
const [count, setCount] = useState(0);
// ❌ PITFALL: Missing dependency
// This will not re-run when count changes
useEffect(() => {
console.log(`Current count is: ${count}`);
}, []); // Missing dependency
return (
<div>
<h2>Count: {count}</h2>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
To avoid this problem, consider using tools such as eslint-plugin-react-hooks
, which can automatically detect missing dependencies.
I’ve noticed some teams provide feedback via Continuous Integration (CI). However, this approach is usually too late. A better practice is to regularly check for these issues in your files using tools like Git commit hooks.
2) Create an Infinite Loop with useEffect
import React, { useState, useEffect } from 'react';
function AutoSaveExample() {
const [text, setText] = useState("Hello");
// ❌ PITFALL: Causes infinite loop
// useEffect(() => {
// setText(text + " (saved)");
// }, [text]);
// ✅ Correct usage: use prev value inside setState
useEffect(() => {
const timeout = setTimeout(() => {
setText(prev => {
if (prev.endsWith(" (saved)")) return prev;
return prev + " (saved)";
});
}, 1000);
return () => clearTimeout(timeout);
}, [text]);
return (
<div>
<h2>Auto-Save Draft</h2>
<input
value={text}
onChange={(e) => setText(e.target.value)}
style={{ width: "300px" }}
/>
</div>
);
}
In the example above, we ensure that the useEffect
hook does not directly update the state based on its dependency (text
) without any condition. By using the previous state value (prev
) inside setText
, we prevent an infinite loop.
3) Cleanup happens before the next effect runs (not just on unmount)
Many developers mistakenly think the cleanup function provided in useEffect
runs only when the component unmounts. However, this isn’t accurate. The cleanup function also runs before the next effect executes on subsequent re-renders.
useEffect(() => {
console.log("Subscribing");
return () => console.log("Cleaning up");
}, [someValue]);
When someValue
changes, React will perform these steps in order:
Execute the cleanup function from the previous render.
Run the effect again.
💡 Want More Tools to Help You Grow?
I love sharing tools and insights that help others grow — both as engineers and as humans.
If you’re enjoying this post, here are two things you shouldn’t miss:
📅 Substack Note Scheduler — This script allows you to schedule Substack Notes using a Notion database. Basic coding experience is required to set this up. It is free to use :)
📚 Everything I Learned About Life — A Notion doc where I reflect on lessons learned, practical tools I use daily, and content that keeps me growing.
👉 Find all resources (and more) here: Start Here