Navigate
Search topics, hooks, and fundamentals
React renders UI. But who handles the music? Timers, subscriptions, events β the DJ booth of your component.
π§ Theme: DJ Booth
Your component is a DJ booth. Rendering is dropping the beat β but side effects (timers, API calls, event listeners) are the equipment setup. useEffect is your roadie: it wires up gear after each performance and tears it down before the next set, keeping the booth clean.
What happens when you drop a beat directly in the component body? Every render starts a new track without stopping the old one β pure noise.
useEffect runs your code AFTER render, and the cleanup function stops the old beat before the next one plays. One clean track at a time.
Beats: 0
1 active β cleanup works!
function DJ() {
const [genre, setGenre] = useState("House");
const [playing, setPlaying] = useState(false);
const [beat, setBeat] = useState(0);
useEffect(() => {
if (!playing) return;
const ms = (60 / genre.bpm) * 1000;
const id = setInterval(() => {
setBeat(b => b + 1);
}, ms);
// Cleanup: stop the old beat before starting the new one
return () => clearInterval(id);
}, [playing, genre]);
// β only re-run when these change
return <Equalizer genre={genre} beat={beat} />;
}useEffect(fn, [])Set up the speakers once
Run on mount, clean up on unmount
useEffect(fn, [track])Switch when the playlist changes
Run when track or BPM changes
useEffect(fn)Restart after every button press
Run after every single render
The dependency array is your setlist β it tells React which knob changes should cue the next track.
useEffect connects React to the outside world β the browser tab, intervals, event listeners, and more. Spin these decks.
Type a track name β watch your browser tab update. Change volume β the effect is skipped.
Now Playing
Sunset Vibes_
Effect ran 0 times
const [trackName, setTrackName] = useState("Sunset Vibes");
const [volume, setVolume] = useState(75);
useEffect(() => {
document.title = `π΅ ${trackName}`;
effectCount.current += 1;
return () => { document.title = "Learn Hooks"; };
}, [trackName]); // only runs when trackName changes
// Volume changes? Effect is SKIPPED.Play the loop, change BPM β the old interval is cleaned up and a new one starts at the new tempo.
Total beats: 0
const [bpm, setBpm] = useState(120);
const [playing, setPlaying] = useState(false);
const [step, setStep] = useState(0);
useEffect(() => {
if (!playing) return;
const ms = (60 / bpm) * 1000;
const id = setInterval(() => {
setStep(s => (s + 1) % 8);
}, ms);
// Cleanup: old interval stops when BPM changes
return () => clearInterval(id);
}, [playing, bpm]);Press any key β the listener was added once on mount and removed on unmount. Always exactly 1 listener.
Press any key...
Keys pressed
0
Active listeners
1
useEffect(() => {
function handleKeyDown(e: KeyboardEvent) {
setLastKey(e.key);
setCount(c => c + 1);
}
window.addEventListener("keydown", handleKeyDown);
// Cleanup: remove on unmount
return () => window.removeEventListener("keydown", handleKeyDown);
}, []); // Empty array = mount only, one listenerThese are the patterns that trip up developers most often. Switch between Wrong and Fixed to compare the code side by side.
function NowPlaying({ trackId }) {
const [track, setTrack] = useState(null);
useEffect(() => {
fetch(`/api/tracks/${trackId}`)
.then(res => res.json())
.then(data => setTrack(data));
}); // no dependency array β runs after EVERY render
return <div>{track?.name}</div>;
}function BeatLoop({ bpm }) {
useEffect(() => {
const id = setInterval(() => {
playBeat();
}, 60000 / bpm);
// no cleanup β interval runs forever, even after unmount
}, [bpm]);
return <div>BPM: {bpm}</div>;
}function Playlist({ userId }) {
const options = { includeHidden: true, userId };
useEffect(() => {
fetchPlaylist(options);
}, [options]); // new object every render = infinite loop!
return <div>Loading...</div>;
}function NowPlaying({ trackId }) {
// async callback returns a Promise, not a cleanup function
useEffect(async () => {
const res = await fetch(`/api/tracks/${trackId}`);
const data = await res.json();
setTrack(data);
}, [trackId]);
}