Appearance
Gotchas that get Vue folks (and everyone else)
A short list of bugs I've hit or watched teammates hit. Caps reserved for the painful ones.
Stale closures
tsx
// broken intuition
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1) // `count` frozen from first render
}, 1000)
return () => clearInterval(id)
}, []) // empty depsWhich now DOESN'T WORK the way you expect - it keeps adding to the initial 0.
Fix with functional updates:
tsx
setCount((c) => c + 1)Or include count in deps (and clear/restart the interval).
Lying to the effect dependency list
If you read userId inside an effect and omit it from deps, you will sync the wrong user. The eslint plugin is annoying because it's often right.
useEffect for derived state
tsx
useEffect(() => {
setFullName(`${first} ${last}`)
}, [first, last])Just compute fullName during render. Extra render cycles and weird timing aren't a personality trait worth keeping.
Index as key on a reordering list
Same as Vue. Reorder / insert → wrong component state sticks to the wrong row. Use stable ids.
Mutating state in place
tsx
todos.push(newTodo)
setTodos(todos) // React may bail out - same referenceNew array: setTodos([...todos, newTodo]).
StrictMode double effects
In development, React mounts, cleans up, mounts again. Your useEffect may run twice. Don't "fix" that by removing cleanup - write cleanup properly instead.
Context value identity thrash
tsx
<ThemeContext.Provider value={{ theme, setTheme }}>New object every render → all consumers re-render. Fine for rare updates; painful for high-frequency values. Selectors in Zustand often feel better for busy state.
Next: 'use client' at the top of everything
That turns your server-first tree into a client bundle party. Push the directive to the smallest interactive leaf.
Next: importing a Server Component into a Client Component
Can't. Pass it as children from a parent Server Component, or split the file.
Next: reading old next/router docs
App Router wants next/navigation. Mixed examples on Google will gaslight you. Check the import path.
Fetch without abort in SPA effects
Race: slow response for user A arrives after user B is on screen. AbortController (or Query) saves you.
Conditional hooks
tsx
if (user) {
useSomething() // 🚫 breaks the rules of hooks
}Hooks at top level only. Same order every render. No debate.
If you remember only three things: immutable updates, honest effect deps, and "derive don't sync". The rest gets easier from there.
Enjoy!