Skip to content

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 deps

Which 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 reference

New 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!