React components are intended to be pure. This means if we give a function the same Props, it should return the exact same JSX, and it shouldn't change anything outside itself.
However, real apps need to talk to the outside world. These actions are called Side Effects:
document.title = ...).setTimeout).Rule: Never perform side effects directly inside the main component body (during render). Use useEffect.
useEffect tells React that our component needs to do something after the render is committed to the screen.
import { useEffect } from "react";
function UserProfile({ userId }) {
useEffect(() => {
// This code runs AFTER the component renders/updates
console.log("Component rendered!");
// Example: Fetch data
fetchData(userId);
});
return <div>User Profile</div>;
}The most important part of useEffect is the second argument: the Dependency Array []. It controls when the effect runs.
| Array Content | Behavior | Lifecycle Equivalent |
|---|---|---|
| No Array | Runs on every render. (Dangerous loop risk!) | componentDidUpdate (always) |
Empty Array [] | Runs only once on mount. | componentDidMount |
Variables [data] | Runs on mount + whenever data changes. | componentDidUpdate (conditional) |
Case A: Run Once (Mount)
useEffect(() => {
console.log("I run only when the component first appears.");
}, []); // Empty arrayCase B: Run on Change
useEffect(() => {
console.log("I run when 'count' changes.");
}, [count]); // Dependency array contains 'count'Sometimes an effect creates a mess that needs cleaning up when the component is removed.
To do this, we return a function from our effect.
useEffect(() => {
// 1. Setup
const timer = setInterval(() => {
console.log("Tick...");
}, 1000);
// 2. Cleanup (Runs when component unmounts OR before re-running effect)
return () => {
clearInterval(timer);
console.log("Timer cleared!");
};
}, []);If we forget this, our app will have memory leaks (ghost timers running in the background).
| Class Method | Functional Hook Equivalent |
|---|---|
componentDidMount | useEffect(() => { ... }, []) |
componentDidUpdate | useEffect(() => { ... }, [prop, state]) |
componentWillUnmount | useEffect(() => { return () => { ... } }, []) |
| Concept | Definition | Key Rule |
|---|---|---|
| Side Effect | Interactions with the outside world (API, DOM). | Never do this in the render body. |
useEffect | Hook to handle side effects. | Runs after the browser paints. |
| Dependency Array | Controls when the effect runs. | Always include variables we use inside. |
| Cleanup Function | Return function from useEffect. | Prevents memory leaks on unmount. |
useEffect (e.g., setCount(count + 1)) but forget to add the dependency array []?We create an Infinite Loop that will crash our browser tab.
useEffect runs (because there is no dependency array, it runs after every render).setCount updates the state.useEffect runs twice on mount. Why does React do this intentionally?React is stress-testing our Cleanup Function.
Mount → Unmount → Mount.