How to prevent unnecessary re-renders in React
Unnecessary re-renders are one of the most common performance bottlenecks in React applications, causing sluggish UIs and poor user experience.
As the creator of CoreUI with over 25 years of software development experience, I’ve optimized dozens of complex React dashboards and component libraries where re-render control was critical.
The most effective solution is wrapping components with React.memo and stabilizing function references with useCallback and values with useMemo.
This prevents child components from re-rendering when their props haven’t actually changed.
Wrap components with React.memo to skip re-renders when props are unchanged.
import { memo } from 'react'
// ❌ Re-renders every time parent renders
function UserCard({ name, email }) {
return (
<div>
<h3>{name}</h3>
<p>{email}</p>
</div>
)
}
// ✅ Only re-renders when name or email changes
const UserCard = memo(function UserCard({ name, email }) {
return (
<div>
<h3>{name}</h3>
<p>{email}</p>
</div>
)
})
React.memo performs a shallow comparison of props. If none of the props changed since the last render, React skips rendering the component and reuses the previous output.
Stabilize Callbacks with useCallback
Inline functions create a new reference on every render, breaking memo.
import { useState, useCallback, memo } from 'react'
function Parent() {
const [count, setCount] = useState(0)
// ❌ New function reference on every render
const handleClick = () => console.log('clicked')
// ✅ Stable reference - won't cause child to re-render
const handleClick = useCallback(() => {
console.log('clicked')
}, [])
return (
<>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<MemoizedChild onClick={handleClick} />
</>
)
}
const MemoizedChild = memo(function MemoizedChild({ onClick }) {
console.log('Child rendered')
return <button onClick={onClick}>Click me</button>
})
useCallback returns the same function reference between renders as long as its dependencies don’t change. Without it, a new function is created on every render, making memo ineffective because the prop reference always changes.
Memoize Expensive Calculations with useMemo
Prevent expensive computations from running on every render.
import { useState, useMemo } from 'react'
function ProductList({ products, filterText }) {
// ❌ Filters entire list on every render
const filtered = products.filter(p =>
p.name.toLowerCase().includes(filterText.toLowerCase())
)
// ✅ Only recalculates when products or filterText changes
const filtered = useMemo(
() => products.filter(p =>
p.name.toLowerCase().includes(filterText.toLowerCase())
),
[products, filterText]
)
return (
<ul>
{filtered.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
)
}
useMemo caches the computed result and only recalculates when the listed dependencies change. Use it for operations that are genuinely expensive — sorting large arrays, complex filtering, or building derived data structures.
Separate State to Limit Re-render Scope
Colocate state close to where it’s used to avoid re-rendering unrelated components.
// ❌ Counter state at top level re-renders everything
function App() {
const [count, setCount] = useState(0)
return (
<>
<ExpensiveChart />
<Counter count={count} onChange={setCount} />
</>
)
}
// ✅ Counter manages its own state - Chart never re-renders
function App() {
return (
<>
<ExpensiveChart />
<Counter />
</>
)
}
function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}
Lifting state only as high as necessary prevents unrelated siblings from re-rendering. If only Counter uses the count value, keep the state inside Counter.
Use Key Prop to Control Re-mounting
Reset component state by changing the key prop instead of clearing state manually.
import { useState } from 'react'
function App() {
const [userId, setUserId] = useState(1)
return (
<div>
<button onClick={() => setUserId(id => id + 1)}>Next User</button>
{/* Changing key unmounts old component and mounts fresh one */}
<UserProfile key={userId} userId={userId} />
</div>
)
}
When the key changes, React unmounts the old component and mounts a new one, giving it a fresh state. This is cleaner than using useEffect to reset state when a prop changes.
Best Practice Note
This is the same optimization strategy we apply in CoreUI React components to keep dashboards with dozens of charts and data tables responsive. Start profiling with React DevTools Profiler before adding memoization — premature optimization adds complexity without benefit. Memoize only when you have a measured performance problem. For large lists, pair React.memo with React virtualization to reduce the number of DOM nodes rendered at once.



