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.

0
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

Định Nguyễn Trương
Định Nguyễn Trương