Best Practices9 min read · Updated 2025-06-01

React Common Mistakes: 8 Bugs That Catch Developers Off Guard

The bugs that appear in React code reviews and interviews. Missing keys, stale closures, direct state mutation, components inside render, and more — with fixes.

💡 Practice these concepts interactively with AI feedback

Start Practicing →

React Common Mistakes: 8 Bugs That Catch Developers Off Guard

1. Defining Components Inside Another Component

// ❌ ComponentB is re-created as a NEW function on every render of Parent
function Parent() {
  function ComponentB() {  // new identity every render
    return <div>B</div>
  }
  return <ComponentB />
}

// What happens: React sees a different component type each render → // unmounts old ComponentB, mounts a new one → state is destroyed every time

// ✅ Define components at module scope
function ComponentB() {
  return <div>B</div>
}

function Parent() { return <ComponentB /> }

This is one of the most common React performance and correctness bugs. It causes components to lose their state on every parent render.

2. Mutating State Directly

const [user, setUser] = useState({ name: 'Alice', scores: [] })

// ❌ Same object reference — React doesn't detect the change user.name = 'Bob' setUser(user)

// ❌ Same array reference — React thinks nothing changed user.scores.push(100) setUser(user)

// ✅ New objects/arrays for new state
setUser({ ...user, name: 'Bob' })
setUser(prev => ({ ...prev, scores: [...prev.scores, 100] }))

Rule: never modify the existing state object. Always return a new reference.

3. Using Array Index as List Key

// ❌ Index as key breaks when list is filtered, sorted, or reordered
{items.map((item, i) => <Card key={i} item={item} />)}

// What breaks: // - React maps state (input values, focus) to position, not item // - Filtering items[0] out causes all subsequent items to get re-rendered // - Input values in position 0 get "transferred" to the new position 0

// ✅ Use stable unique IDs
{items.map(item => <Card key={item.id} item={item} />)}

Index keys are only safe for static, never-reordered lists with no stateful children.

4. Missing useEffect Cleanup

// ❌ Subscription grows on every userId change; old subscriptions never removed
useEffect(() => {
  const socket = openConnection(userId)
  socket.on('message', handleMessage)
  // no cleanup!
}, [userId])

// What happens: Every userId change adds a NEW socket and listener. // After 5 userId changes you have 5 active subscriptions.

// ✅ Return cleanup function
useEffect(() => {
  const socket = openConnection(userId)
  socket.on('message', handleMessage)
  return () => {
    socket.off('message', handleMessage)
    socket.close()
  }
}, [userId])

5. Stale Closure in useEffect

const [count, setCount] = useState(0)

// ❌ count is captured as 0 at mount — forever stale useEffect(() => { const id = setInterval(() => { console.log(count) // always prints 0 setCount(count + 1) // always sets to 1! }, 1000) return () => clearInterval(id) }, []) // missing [count]

// ✅ Option A: functional update (no need to read count from closure)
useEffect(() => {
  const id = setInterval(() => {
    setCount(prev => prev + 1)  // always gets latest count
  }, 1000)
  return () => clearInterval(id)
}, [])

// ✅ Option B: include count in deps (effect re-subscribes when count changes) useEffect(() => { const id = setInterval(() => { setCount(count + 1) }, 1000) return () => clearInterval(id) }, [count])

6. Calling Hooks Conditionally

// ❌ Hook order changes between renders — React throws an error
function Component({ isLoggedIn }) {
  if (isLoggedIn) {
    const [name, setName] = useState('')  // hook #1 sometimes, sometimes not
  }
  const [age, setAge] = useState(0)  // sometimes hook #1, sometimes hook #2
}
// ✅ Hooks always at top level; conditionally USE their values
function Component({ isLoggedIn }) {
  const [name, setName] = useState('')
  const [age, setAge] = useState(0)

if (!isLoggedIn) return <Login /> return <Profile name={name} age={age} /> }

7. Storing Derived State in useState

// ❌ Derived state — two sources of truth get out of sync
const [items, setItems] = useState([...])
const [filteredItems, setFilteredItems] = useState(items)  // redundant

useEffect(() => { setFilteredItems(items.filter(i => i.active)) }, [items]) // easy to forget, causes a render lag

// ✅ Compute derived values during render (free, always in sync)
const [items, setItems] = useState([...])
const filteredItems = items.filter(i => i.active)  // computed every render

// ✅ Or memoize if the computation is expensive const filteredItems = useMemo(() => items.filter(i => i.active), [items])

8. Forgetting that setState is Asynchronous

// ❌ Reading state immediately after setting it gets the stale value
const [count, setCount] = useState(0)

function handleClick() { setCount(count + 1) console.log(count) // still 0! state updates are scheduled, not immediate setCount(count + 1) // also adds 1 to the ORIGINAL 0 → count becomes 1, not 2! }

// ✅ Use functional updates for multiple sequential updates
function handleClick() {
  setCount(prev => prev + 1)
  setCount(prev => prev + 1)  // this correctly uses the updated value → +2
}

Practice at [JSPrep Pro](/auth).

📚 Practice These Topics
React Component Lifecycle
10–15 questions
Custom hook
4–8 questions
Concurrent Rendering React 18
4–8 questions
React Virtual DOM
8–12 questions

Put This Into Practice

Reading articles is passive. JSPrep Pro makes you actively recall, predict output, and get AI feedback.

Start Free →Browse All Questions

Related Articles

Deep Dive
We Built a RAG-Powered AI Question Engine Into a JavaScript Interview Platform — Here's Exactly How It Works
12 min read
Build Systems
Monorepo with Turborepo vs Nx: The Complete Comparison (2025)
9 min read
Core Concepts
map() vs forEach() in JavaScript: Which One to Use and Why It Matters
7 min read