When working with React, handling side effects correctly can make or break your component’s performance, readability, and predictability. Side effects—like data fetching, subscriptions, or DOM manipulations—shouldn’t be sprinkled all over your codebase like confetti.
Here’s a deep dive into the best practices every serious React dev should follow when dealing with side effects.
⚠️ First, What Are Side Effects in React?
Side effects are operations that affect something outside the scope of the current function being executed. In React, they can include:
- Fetching data from APIs
- Manually modifying the DOM
- Subscribing to WebSocket or event listeners
- Setting timeouts or intervals
- Logging or analytics tracking
These are not part of the component’s render output and must be handled outside the render phase.
✅ 1. Use useEffect
for Declarative Side Effects
React's useEffect
hook is the official home for most side effects in functional components.
useEffect(() => {
const fetchData = async () => {
const res = await fetch("/api/data");
const data = await res.json();
setData(data);
};
fetchData();
}, []);
Best practice:
Use the dependency array correctly. It should contain everything your effect depends on.
✅ 2. Separate Concerns — Don't Cram Everything Into One Effect
Avoid the “God effect” trap where one useEffect
tries to do it all. Split effects by purpose: data fetching, subscriptions, cleanup logic, etc.
useEffect(() => {
// Handles window resize
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
useEffect(() => {
// Fetch data separately
}, []);
✅ 3. Always Clean Up Subscriptions and Timeouts
If you don’t clean up, you’ll leak memory and trigger bugs during unmounting or re-renders.
useEffect(() => {
const timer = setTimeout(() => {
console.log("Delayed log");
}, 1000);
return () => clearTimeout(timer);
}, []);
✅ 4. Avoid Side Effects During Rendering
Never call fetch()
, setTimeout()
, or manipulate the DOM directly during render.
🚫 Bad:
const MyComponent = () => {
fetch("/api/data"); // NOPE
return <div>Hello</div>;
};
✅ Good:
useEffect(() => {
fetch("/api/data");
}, []);
✅ 5. Use Custom Hooks to Encapsulate Reusable Side Effects
If you're repeating effect logic, it’s time to extract it into a custom hook.
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
return width;
}
Then use it like:
const width = useWindowWidth();
✅ 6. Be Mindful of Async Functions in useEffect
You can't pass an async function directly to useEffect
. Instead, define an async function inside it and call it.
useEffect(() => {
const loadData = async () => {
try {
const res = await fetch("/api/data");
const json = await res.json();
setData(json);
} catch (err) {
console.error(err);
}
};
loadData();
}, []);
✅ 7. Don’t Use Effects for State Derivation
If your state can be derived from props or other state, do it in the render—not with side effects.
// ✅ Good
const derivedState = someValue > 0;
// 🚫 Bad
useEffect(() => {
setDerivedState(someValue > 0);
}, [someValue]);
🧠 Final Thoughts
Side effects are where bugs love to hide. Clean, scoped, and well-structured effects make your code:
- Easier to debug
- Easier to test
- More predictable
And in the React world, predictability is power.
Top comments (0)