Side effects with useEffect
Rendering should only describe what's on screen. Anything else, such as starting a timer, fetching data, listening to the keyboard or changing the page title, is a side effect, and belongs in useEffect.
import { useEffect, useState } from "react";
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Clicked ${count} times`;
console.log("effect ran, count is", count);
});
return <button onClick={() => setCount(count + 1)}>Clicked {count} times</button>;
}The effect runs after React has updated the screen. With no second argument, it runs after every render: watch the console as you click.
The dependency array
useEffect(() => { ... }); // after every render
useEffect(() => { ... }, []); // once, after the first render
useEffect(() => { ... }, [userId]); // after the first render, and whenever userId changesimport { useEffect, useState } from "react";
function App() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
useEffect(() => { console.log("runs once, after the first render"); }, []);
useEffect(() => { console.log("a changed to", a); }, [a]);
return (
<div>
<button onClick={() => setA(a + 1)}>a = {a}</button>{" "}
<button onClick={() => setB(b + 1)}>b = {b}</button>
<p>Click both and watch the console: changing b never runs the "a changed" effect.</p>
</div>
);
}List every value from the component that the effect uses. React then re-runs the effect exactly when one of them changes.
Cleaning up
import { useEffect, useState } from "react";
function Clock() {
const [now, setNow] = useState(new Date());
useEffect(() => {
const id = setInterval(() => setNow(new Date()), 1000);
return () => clearInterval(id); // the cleanup: stop the timer
}, []);
return <h2>{now.toLocaleTimeString()}</h2>;
}
function App() {
const [show, setShow] = useState(true);
return (
<div>
<button onClick={() => setShow(!show)}>{show ? "Hide" : "Show"} the clock</button>
{show && <Clock />}
</div>
);
}The function an effect returns is its cleanup. React calls it when the component disappears, and before running the effect again. Without it, a hidden clock's timer would keep ticking in the background for as long as the page stays open.
Listening to the keyboard
import { useEffect, useState } from "react";
function App() {
const [key, setKey] = useState("(none yet)");
useEffect(() => {
const onKey = (e) => setKey(e.key === " " ? "Space" : e.key);
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, []);
return <p>Click inside this preview, then press any key. Last key: <strong>{key}</strong></p>;
}Listeners on window or document live outside React, so the effect adds the listener and its cleanup removes it.
Fetching data
import { useEffect, useState } from "react";
// a pretend server: answers after 800 ms, and fails for an unknown id
function fetchCourse(id) {
const courses = { 1: { title: "Python", lessons: 64 }, 2: { title: "SQL", lessons: 23 }, 3: { title: "React", lessons: 12 } };
return new Promise((resolve, reject) =>
setTimeout(() => (courses[id] ? resolve(courses[id]) : reject(new Error("Course not found"))), 800)
);
}
function App() {
const [id, setId] = useState(1);
const [course, setCourse] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let ignore = false; // set when id changes before the answer arrives
setLoading(true);
setError(null);
fetchCourse(id)
.then((data) => { if (!ignore) setCourse(data); })
.catch((err) => { if (!ignore) setError(err.message); })
.finally(() => { if (!ignore) setLoading(false); });
return () => { ignore = true; };
}, [id]);
return (
<div>
{[1, 2, 3, 4].map((n) => (
<button key={n} onClick={() => setId(n)} style={{ marginRight: 6, fontWeight: n === id ? 700 : 400 }}>Course {n}</button>
))}
{loading ? <p>Loading...</p>
: error ? <p style={{ color: "crimson" }}>{error}</p>
: <p>{course.title}: {course.lessons} lessons</p>}
</div>
);
}A real app calls fetch(url) in the same place. The pretend server just keeps the example working without a network:
useEffect(() => {
let ignore = false;
fetch(`https://api.example.com/courses/${id}`)
.then((r) => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
.then((data) => { if (!ignore) setCourse(data); })
.catch((err) => { if (!ignore) setError(err.message); });
return () => { ignore = true; };
}, [id]);The ignore flag handles a race. Click quickly from course 1 to course 2, and course 1's slower answer could arrive last and overwrite course 2's. The cleanup marks the old request as one to ignore.