How to prevent unnecessary re-renders in React
Unnecessary re-renders are one of the most common performance issues in React applications, causing components to update even when their data hasn’t changed. With over 10 years of experience building React applications and as the creator of CoreUI, I’ve optimized countless components to prevent wasteful re-renders. From my expertise, the most effective approach is to use React.memo for functional components combined with useMemo and useCallback hooks to memoize values and functions. This method is reliable, easy to implement, and can dramatically improve application performance.
Use React.memo to wrap functional components and prevent re-renders when props haven’t changed.
import { memo } from 'react'
const ExpensiveComponent = memo(({ data }) => {
return <div>{data.name}</div>
})
How It Works
React.memo is a higher-order component that memoizes the result of a component render. When the parent component re-renders, React will skip rendering the memoized component if its props haven’t changed. In the example above, ExpensiveComponent will only re-render when the data prop changes, not when its parent re-renders for other reasons.
Using useMemo for Expensive Calculations
When you have expensive calculations that depend on specific values, use useMemo to cache the result:
import { useMemo } from 'react'
const DataList = ({ items }) => {
const sortedItems = useMemo(() => {
return [...items].sort((a, b) => a.value - b.value)
}, [items])
return (
<ul>
{sortedItems.map((item) => (
<li key={item.id}>{item.value}</li>
))}
</ul>
)
}
The useMemo hook only recalculates sortedItems when the items array changes. Without useMemo, the sorting would happen on every render, even if items hasn’t changed. The dependency array [items] tells React to only recompute when that specific value changes.
Using useCallback for Event Handlers
Event handlers create new function instances on every render, which can cause child components to re-render unnecessarily:
import { useCallback } from 'react'
const ParentComponent = () => {
const [count, setCount] = useState(0)
const handleClick = useCallback(() => {
setCount((prev) => prev + 1)
}, [])
return <ChildComponent onClick={handleClick} />
}
const ChildComponent = memo(({ onClick }) => {
return <button onClick={onClick}>Click me</button>
})
The useCallback hook memoizes the handleClick function so it maintains the same reference across renders. Without useCallback, a new function would be created on every render, causing ChildComponent to re-render even though it’s wrapped in memo.
Custom Comparison Function
For complex props, provide a custom comparison function to React.memo:
const UserCard = memo(
({ user }) => {
return (
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
)
},
(prevProps, nextProps) => {
return prevProps.user.id === nextProps.user.id
}
)
The second argument to memo is a comparison function that returns true if the props are equal (skip render) or false if they’re different (render). This is useful when you only care about specific properties of an object, like the id in this example, rather than doing a shallow comparison of the entire object.
Using Key Prop Strategically
The key prop can control whether React reuses or recreates a component:
const TabContent = ({ activeTab, data }) => {
return <ContentPanel key={activeTab} data={data} />
}
By using activeTab as the key, React will unmount and remount ContentPanel whenever the active tab changes, resetting its internal state. This is useful when you want a component to start fresh with new props.
Splitting State and Components
Keep state as close as possible to where it’s used to minimize re-render scope:
const Dashboard = () => {
return (
<div>
<SidebarWithCounter />
<MainContent />
</div>
)
}
const SidebarWithCounter = () => {
const [count, setCount] = useState(0)
return (
<aside>
<button onClick={() => setCount(count + 1)}>{count}</button>
</aside>
)
}
By moving the count state into SidebarWithCounter, updates only trigger re-renders of that component, not the entire Dashboard. This component composition strategy is more effective than trying to memoize everything.
Using React DevTools Profiler
Identify unnecessary re-renders using the React DevTools Profiler:
import { Profiler } from 'react'
const App = () => {
const onRenderCallback = (
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime
) => {
console.log(`${id} took ${actualDuration}ms to render`)
}
return (
<Profiler id='App' onRender={onRenderCallback}>
<Dashboard />
</Profiler>
)
}
The Profiler component measures how often and how long components take to render. The onRenderCallback receives timing information that helps identify performance bottlenecks. Use this in development to find components that re-render too frequently.
Best Practice Note
This is the same performance optimization approach we use in CoreUI React components to ensure smooth, responsive user interfaces even with large datasets. The key is to measure first using React DevTools, then optimize strategically. Don’t wrap every component in memo - only optimize components that actually have performance issues. Premature optimization can make code harder to maintain without providing real benefits. For building complex UIs, see our guide on how to build a dashboard in React which demonstrates these optimization techniques in a real-world scenario. For data tables, consider using CoreUI’s CDataTable component which includes built-in performance optimizations.



