The first React app I wrote with hooks had a useEffect that fetched a user profile. It worked on the first load. Then I added the user ID to the dependency array because the linter told me to, and the network tab filled up with the same request, over and over, until the tab froze.
I fixed it by deleting the dependency array. That was the wrong fix, and it took me an embarrassingly long time to understand why.
Most people learn hooks the way I did: copy a pattern, hit a bug, paste a Stack Overflow answer. You end up knowing a dozen workarounds and none of the ideas underneath. Here's the order I'd learn them in now.
Start with what a render actually is
Before any hook makes sense, you need one mental model. Every time state changes, React calls your component function again, top to bottom. Every variable inside it is created fresh. Every function you define inside it is a new function.
That's why this logs a stale value:
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
console.log(count); // still the old number
}
return <button onClick={handleClick}>{count}</button>;
}
count belongs to the render that created handleClick. It isn't a live variable that updates in place. The new value only exists in the next render. Once that clicked for me, half my "hooks are weird" complaints went away. They weren't weird. I just thought my component was an object that stuck around, and it's really a function that runs again.
useState: learn the updater form early
You'll use useState constantly, so learn the one detail that bites people. If the new state depends on the old state, pass a function:
setCount(c => c + 1);
Call setCount(count + 1) three times in one handler and you get +1, because all three read the same count. Call setCount(c => c + 1) three times and you get +3. I'd also learn that objects in state get replaced, not merged, so setUser({ ...user, name }) is the habit to build.
useEffect is for syncing with things outside React
This is where most of the pain is, and the React docs rewrite at react.dev is the best thing I've read on it. The short version: an effect is for keeping your component in sync with something React doesn't control. A network request, a subscription, a timer, a DOM API, a third-party widget.
The dependency array isn't a list of "when to run". It's a list of every value from the render that the effect reads. If you lie to it, you get stale data. If you include an object or function that's recreated every render, you get my infinite loop.
The fixes I reach for, in order:
- Move the function inside the effect, so it isn't a dependency at all.
- Depend on primitive values (
user.id) instead of whole objects (user). - Ask whether this needs to be an effect in the first place.
That third one matters most. The docs page You Might Not Need an Effect is worth reading twice. Filtering a list, computing a total, resetting a form when a prop changes: none of these need useEffect. You can calculate them during render. A lot of my old bugs came from effects that set state that could have just been a variable.
And write the cleanup. If an effect subscribes, starts a timer or opens a socket, return a function that undoes it. In development, Strict Mode runs your effects twice on mount to flush out missing cleanups, which is why your fetch shows up twice in the console. That's on purpose.
useRef, useMemo and useCallback can wait
useRef is simple once you see it as a box that survives renders without causing one. Use it for DOM nodes and for values you need to remember but never display, like an interval ID.
useMemo and useCallback are performance tools. I'd skip them until you have a real reason, like a slow calculation or a memoized child component that keeps re-rendering. Wrapping everything in them makes code harder to read and usually doesn't make it faster.
Custom hooks come last, and they're just functions that start with use and call other hooks. The first time you pull a useDebounce or useLocalStorage out of two components that both had the same effect, you'll get why they exist.
Build small things that break
Reading about hooks only gets you so far. These four tiny projects cover almost every common mistake:
- A counter with a "+3" button, to learn the updater form.
- A search box that fetches results as you type, to learn dependencies, cleanup and race conditions.
- A stopwatch with start and stop, to learn
useReffor the interval ID. - A dark mode toggle saved to
localStorage, pulled into a custom hook.
Do them without a tutorial open. When something loops or shows stale data, that's the lesson.
Then make it stick
My actual problem with hooks wasn't understanding them once. I'd read the effects docs, get it, and a month later on a different project write the same broken fetch again. The rules of the dependency array are the kind of thing you know in theory and forget in practice.
What helped was reviewing the handful of ideas that kept tripping me up: renders are snapshots, the updater form, dependencies are what the effect reads, maybe you don't need an effect. I'd look at those notes after a day, then a few days, then a couple of weeks. That's spaced repetition, and it works better for this than rereading a tutorial when you're already stuck.
I built Recl for exactly this kind of learning. You share a video, article, reel or podcast to it, say a conference talk on effects or the react.dev page itself, and it writes a summary of the key points, files it under a topic like "React", and brings it back for review on a spaced repetition schedule. A plain notes file with calendar reminders does the same job if you'd rather not use an app. What matters is seeing it again before the next bug does the reminding for you.
I build Recl, an AI learning app that saves your videos, reels, podcasts and articles, summarizes them, sorts them into topics, and uses spaced repetition so you remember what you learned.
Top comments (0)