Mount, update, and unmount phases β see which hooks and effects fire at each stage.
Every React component goes through three lifecycle phases: Mount (first render into the DOM), Update (re-renders due to state or prop changes), and Unmount (removal from the DOM). With hooks, these phases are expressed through useEffect, useLayoutEffect, and cleanup functions rather than class lifecycle methods.
React calls your function component, evaluates JSX, and inserts the resulting DOM nodes. useState initializers run, useRef objects are created. After DOM insertion, useLayoutEffect runs synchronously, then useEffect runs asynchronously after paint.
When setState or a new prop arrives, React queues a re-render. During re-render, hooks are called in the same order as on mount. React compares new vs old output and patches the DOM. Before re-running effects, React runs cleanup functions from the previous render's effects.
Before each effect re-runs (and on unmount), React calls the previous effect's cleanup function. This is crucial for clearing timers, canceling fetches, and unsubscribing from events.
When a component is removed (conditional render, route change, etc.), React runs all cleanup functions β useEffect cleanup, then useLayoutEffect cleanup. DOM nodes are removed from the document.
Empty deps array: runs once after mount. Cleanup runs on unmount. Equivalent to componentDidMount + componentWillUnmount.
Runs after mount AND whenever dep changes. Cleanup runs before each re-run and on unmount.
No deps array: runs after every render. Rarely what you want β causes infinite loops if it triggers state changes.
Same signature as useEffect but fires synchronously after DOM mutations, before paint. Use for DOM measurements.
The function returned from a useEffect callback. React calls it before the next effect run and on unmount.
1function Component({ userId }) {2 const [data, setData] = useState(null);34 useEffect(() => {5 // Runs after mount and when userId changes6 let cancelled = false;78 fetch(`/api/user/${userId}`)9 .then(res => res.json())10 .then(json => {11 if (!cancelled) setData(json); // guard against stale closure12 });1314 // Cleanup: cancel if userId changes before fetch resolves15 return () => { cancelled = true; };16 }, [userId]); // re-runs when userId changes1718 // Runs on unmount19 useEffect(() => {20 return () => console.log('Component unmounted');21 }, []);22}
Understanding lifecycle timing prevents the most common React bugs: fetching data without cleanup (causes setState on unmounted component warnings), forgetting to clear timers (causes memory leaks), and running effects too often or too rarely by misusing the dependency array.
A user profile page fetches user data on mount. If a user rapidly clicks through profiles (User A β User B β User C), the displayed data sometimes shows User A's data on User C's page.
Each navigation triggers a re-mount with a new userId. The useEffect fires 3 fetch calls, but they resolve out of order: User B (fast) β User C (fast) β User A (slow). The last-resolving fetch (User A) overwrites the correct data.
Use the cleanup function to set a `cancelled` flag. When userId changes, the previous effect's cleanup runs before the new effect, marking the old fetch as stale. The guard `if (!cancelled) setData(json)` prevents stale responses from updating state.
Takeaway: The cleanup function in useEffect isn't just for unmounting β it runs before EVERY re-execution of the effect. This makes it the perfect place to cancel async operations and prevent race conditions.
A chart component adds a `window.resize` event listener on mount to recalculate dimensions. After navigating away, the browser's memory usage keeps climbing and console shows 'setState on unmounted component' warnings.
The event listener was added in useEffect but never cleaned up. The component unmounts, but the listener still holds a reference to the component's state setter, creating a memory leak and triggering warnings on resize.
Return a cleanup function from useEffect that removes the event listener. The cleanup runs during the Unmount phase, before DOM removal, ensuring no orphaned listeners persist. Apply the same pattern for WebSocket connections, interval timers, and IntersectionObserver instances.
Takeaway: Every subscription, listener, or timer created in useEffect MUST have a corresponding cleanup. Think of mount/unmount as a pair: every 'set up' action needs a corresponding 'tear down' action.