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).