Optimize your react component with useMemo, useCallback, useTransition
Hook provides convenient ways to build your react application with functional components.
When your React application grows, unnecessary re-renders can hurt the app's performance. Luckily there are some builtin hooks to solve only that problem for us.
Today, we’ll explore some powerful hooks that can boost your app performance: useMemo, useCallback and useTransition
useMemo: Memoizing computed value
When the app growing, business logic can become more complex and computationally expensive. Those logics could lie anywhere in your component, and every time it re-renders, all those logics will be recalculated - even if it don’t have to.
Consider the example below:
function DataVisualizer({ data, threshold }) {
const processedData = data.map(item => item.value)
.filter(value => value > threshold);
return (
<div>
<h2>Data points above threshold: {processedData.length}</h2>
{/* Render chart using processedData */}
</div>
);
}
Every time DataVisualizer
re-render, the filter will be called again, even if data
and threshold
value doesn’t change.
How to memoize the result? We can use useMemo
function DataVisualizer({ data, threshold }) {
const processedData = useMemo(() => {
return data.map(item => item.value)
.filter(value => value > threshold);
}, [data, threshold]);
return (
<div>
<h2>Data points above threshold: {processedData.length}</h2>
{/* Render chart using processedData */}
</div>
);
}
From now on, only when data
and threshold
change, the filter function will be called, otherwise, the old value will be used when the component re-render. That could save your component some milliseconds of rendering time.
Noted: You don’t need to useMemo on simple cases, where memoize logic could be slower than the original.
useCallback: Memoizing function definition
You need memoize your function on a special case.
Consider this example:
export function ProductPage({productId, theme}) {
const handleSubmit = (orderDetails) => {
post('/product/' + productId + '/buy', {
orderDetails,
});
};
return (
<div className={theme}>
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
Every time theme
change, the whole ProductPage
re-renders, make the ShippingForm
re-render with it. But what if ShippingForm
is requires much calculation to re-render, you can tell React to only re-render it when it’s function props onSubmit
change by using memo
:
const ShippingForm = memo(function ShippingForm({ onSubmit }) {
// ...
});
But the problem isn’t gone yet
Every time theme
change, handleSubmit
will be redefined again, making it a whole new function (even when the function content is the same)
And that’s when you need useCallback
export function ProductPage({productId, theme}) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
orderDetails,
});
}, [productId]);
return (
<div className={theme}>
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
By wrapping it using useCallback
, you ensure that only when productId
change, handleSubmit
will be redefined. Otherwise, it is the same between re-renders.
And that means only when productId
change, ShippingForm
will be re-rendered.
useTransition: Prioritizing UI Updates
Sometimes a component could take a while to render, and you don’t want that process freezing the whole app. You can prevent it by using useTransition
Let’s take a look at the example based on example of React official doc
function TabContainer() {
const [tab, setTab] = useState('about');
function selectTab(nextTab) {
setTab(nextTab);
}
return (
<>
<TabButton
isActive={tab === 'about'}
onClick={() => selectTab('about')}
label={'About'} />
<TabButton
isActive={tab === 'posts'}
onClick={() => selectTab('posts')}
label={'Posts'} />
{tab === 'about' && <AboutTab />}
{tab === 'posts' && <PostsTab />}
</>
);
}
Our UI is a tab layout with Post and About tab. Let’s say the <PostsTab />
takes 500ms to render. So when user chooses the Post tab, the whole UI will freeze in 500ms, they can’t do anything, if user wants to switch to another tab immediately, they have to wait.
To mark this process non-UI blocking, use useTransition
like this
function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('about');
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
return (
<>
<TabButton
isActive={tab === 'about'}
onClick={() => selectTab('about')}
label='About' />
<TabButton
isActive={tab === 'posts'}
onClick={() => selectTab('posts')}
label='Posts' />
{tab === 'about' && <AboutTab />}
{tab === 'posts' && <PostsTab />}
</>
);
}
That’s it, startTransition
now will take care of the non-blocking UI logic. Let your user switch between tabs without any interruptions.
Recap
So, in this post, we explore three powerful built-in hooks in React. It will help improve your app performance when used the right way.
useMemo: This hook helps memoize computed values, preventing expensive calculations from being re-executed on every render.
useCallback: This hook is used to memoize function definitions. It ensures that functions are not redefined on every render unless their dependencies change.
useTransition: This hook allows you to prioritize UI updates, making sure that the UI remains responsive even when some components take a while to render.
Subscribe to my newsletter
Read articles from Định Nguyễn Trương directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by