Effects and Strict Mode
Introduction
Side effects—fetching data, subscriptions, timers, syncing with document.title—belong outside the render path. useEffect runs code after render and can clean up when dependencies change or the component unmounts. Strict Mode helps catch unsafe effects during development.
Prerequisites
- State: useState and useReducer
- Basic understanding of JavaScript Promises
useEffect Basics
import { useEffect, useState } from "react";
function PageTitle({ title }: { title: string }) {
useEffect(() => {
document.title = title;
}, [title]);
return <h1>{title}</h1>;
}Code explanation:
useEffect(fn, deps)runsfnafter paint[title]— re-run whentitlechanges[]— run once after mount (and clean up on unmount)- Omitting deps — runs after every render (rarely what you want)
Cleanup Functions
useEffect(() => {
const id = window.setInterval(() => {
console.log("tick");
}, 1000);
return () => {
window.clearInterval(id);
};
}, []);Code explanation:
return () => { ... }runs before the effect re-runs and on unmount- Cancel timers, abort fetches, remove event listeners here
Event listener example:
useEffect(() => {
function onResize() {
setWidth(window.innerWidth);
}
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, []);Fetching Data (Simple Pattern)
Code explanation:
cancelledflag ignores stale responses whenuserIdchanges quickly- Later you will wrap this in
useUsercustom hook—Custom Hooks
Common Mistakes
Infinite loop
// Wrong — setState in effect with no proper deps
useEffect(() => {
setCount(count + 1);
});Fix: add correct dependencies or use functional updates only when intended.
Missing dependency
ESLint react-hooks/exhaustive-deps warns when values used inside the effect are omitted from the dependency array.
Object/array deps
// New object every render — effect runs every time
useEffect(() => {}, [{ id: userId }]);Depend on primitives: [userId].
Mapping to Class Lifecycle (Legacy)
| Class | Hooks |
|---|---|
componentDidMount | useEffect(() => {}, []) |
componentDidUpdate | useEffect(() => {}, [dep]) |
componentWillUnmount | cleanup in useEffect return |
Vue mapping: onMounted / watch — Vue lifecycle.
Strict Mode
main.tsx wraps the app:
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>
);In development, Strict Mode:
- Double-invokes some functions to surface impure render logic
- Mounts, unmounts, and remounts components to test effect cleanup
You may see fetch run twice in dev—that is intentional. Production builds do not double effects.
Tip
Idempotent Effects
Write effects that tolerate re-run: use cleanup, abort controllers, and ignore stale async results.
useEffect vs Event Handlers
| Use effect | Use event handler |
|---|---|
| Sync with external system after render | Respond to user click/submit |
| Subscribe to browser/window events | Update state directly on interaction |
Fetch when id prop changes | POST form on submit |
Do not put every state update in an effect—handle user input in handlers.
FAQ
Can I make useEffect async?
// Wrong
useEffect(async () => { ... }, []);
// Right
useEffect(() => {
async function load() { ... }
load();
}, []);useLayoutEffect?
Runs before browser paint—measure DOM layout. Rare; default to useEffect.
No cleanup needed?
Omit return—effect simply runs and finishes.
Strict Mode in production?
Wrapped app is fine—extra checks are dev-only.
Replace useEffect with React Query?
Libraries like TanStack Query handle caching and retries—optional after you understand raw fetch in chapter 16.