Heading image for post: React Performance with Hooks

React

React Performance with Hooks

Profile picture of Vinicius Negrisolo

In this post we'll go through some hidden performance implications of using custom React Hooks. This is all about the journey of building a custom hook and how can we make it great to use and maintain.

Background

React Hooks API was introduced to the public in 2018, and since the beginning it caused a lot of noise in the React community.

Some developers love it; the API seems so simple to use, with just a few building block functions to understand and use. It's all functional, so no more mixing between functional and class React components anymore. Also custom hooks allow developer to share logic that involves class components callbacks much easier.

On the other hand there are some developers that hate it; holding state in functional components seems wrong, classes are the abstraction to go for that. Custom hooks are very often extract from components without the real need, with not so great abstractions hence maintaining hooks could be cost-y.

At this far, hooks are very spread in most of React apps, so we'll jump directly into the problem.

The Problem

Developers constantly need to use current date and time in their apps. It could be for displaying, or to compare with other dates in the future or past, etc. For this blog post we will go over a hypothetical app that the main feature is to count down for the next year. Let's use create-react-app to start off:

npx create-react-app happy-new-year --template typescript

And here an image of what we want to build:

Happy New Year App

As you can see we'll have a simple title section and two main components, the Clock and the NewYearCountdown component.

The Clock component

This component has to get the current date and time and display it, simple as that.

import React, { useEffect, useState } from 'react';

const Clock = () => {
  const [now, setNow] = useState<Date>(new Date());

  useEffect(() => {
    const intervalId = setInterval(() => setNow(new Date()), 1000);

    return () => clearInterval(intervalId);
  }, []);

  return (
    <>
      <h3>Clock</h3>
      <div>{now.toLocaleString()}</div>
    </>
  );
}

export default Clock;

The useNow hook

We'll quickly refactor that component to extract the setInterval logic into a new hook. We definately don't need that yet, but we saw an opportunity and we are thinking that this might be reused soon in the code, so let's early engineering that:

import { useEffect, useState } from 'react';

const useNow = () => {
  const [now, setNow] = useState<Date>(new Date());

  useEffect(() => {
    const intervalId = setInterval(() => {
      setNow(new Date());
    }, 1000);

    return () => {
      clearInterval(intervalId);
    };
  }, []);

  return now;
}

export default useNow;

This is a very common hook actually used to explain how to create hooks, so it's very straighforward and most react devs had seen this already. This hook is responsible to setup and clear down the interval, and on each tick of the clock, we set a new state to force the component that uses this hook to re-render with a new now. We decided to return the now javascript Date and that's why we named this hook as useNow. We are going to use that hook right away in the current Clock component:

import React from 'react';
import useNow from '../hooks/useNow';

const Clock = () => {
  const now = useNow();

  return (
    <>
      <h3>Clock</h3>
      <div>{now.toLocaleString()}</div>
    </>
  );
};

export default Clock;

This is much clear now, our Component is very simple, the set interval and setting the state is inside the hook, so it seems all very good so far. Let's continue with the NewYearCountdown:

The NewYearCountdown component

The NewYearCountdown component also needs to render every 1 second, so using the useNow hook seems the right choice so far:

import React from 'react';
import useNow from '../hooks/useNow';

const NewYearCountdown = () => {
  const now = useNow();
  const nextYear = now.getFullYear() + 1;
  const turnaroundTime = new Date(nextYear, 0, 1, 0, 0, 0, 0);
  const countdown = Math.trunc((turnaroundTime.getTime() - now.getTime()) / 1000);
  const countdownSec = countdown % 60;
  const countdownMin = Math.trunc(countdown / 60 % 60);
  const countdownHours = Math.trunc(countdown / (60 * 60) % 24);
  const countdownDays = Math.trunc(countdown / (60 * 60 * 24));

  return (
    <div>
      <h3>Count down to {nextYear}</h3>
      <div>{countdownDays} days</div>
      <div>{countdownHours} hours</div>
      <div>{countdownMin} minutes</div>
      <div>{countdownSec} seconds</div>
    </div>
  );
}

export default NewYearCountdown;

On that component we're breaking down how many days, hours, minutes and seconds for the next year.

Components out of sync

At this point we may notice that the Clock component and the NewYearCountdown component seem to be not synced with each other. In other words the components won't render exactly at the same time. This happens for two main reasons:

  1. We are using 2 different calls on useNow, so each call will set a different state when it runs for the first time, hence the two setIntervals will not be synced together.
  2. React will render components as they need based in so many factors like state changes, memoizations, etc. This might cause a delay when React apply the changes to the DOM, even if we'd be using the same useNow state.

Said all that, let's assume that the business are ok with this and they want to move on by also adding a subtitle to the header of the app based on how far we are to the new year's eve.

The Greeting Message

So business asked the team to add a greeting message to the home page just bellow the title, with some rules based on the current month. Let's see how we have implemented:

import React from 'react';
import './App.css';
import Clock from './components/Clock';
import NewYearCountdown from './components/NewYearCountdown';
import useNow from './hooks/useNow';

const greetingMessage = (now: Date) => {
  const month = now.getMonth() + 1;

  if (month <= 1) {
    return `Happy new Year!!! ${now.getFullYear()} will be amazing!`
  }
  if (month >= 11) {
    return `Counting down to the ${now.getFullYear() + 1} year!!!`
  }
  return "This app is usually not used this time of the year, so come back later.";
}

const App = () => {
  const now = useNow();
  const message = greetingMessage(now);

  return (
    <div>
      <h1>Happy New Year</h1>
      <h3>{message}</h3>
      <div>
        <Clock />
        <NewYearCountdown />
      </div>
    </div>
  );
};

export default App;

As we can see we extracted the greeting message into a function outside the functional component itself, which is really cool. It uses the current date to extract the month and year to build the message. As we needed the current date to show a proper greeting message we decided to use the existing hook useNow.

Re-Rendering Everywhere

Let's investigate the performance of our app so far with the chrome plugin React Developer Tools using the Profiler tab, then we checked the option to highlight re-renders and here it is:

Happy New Year Re-Render

Wow, that's a lot re-renderings happening in such short period of time, and even for very static components like the Footer component. At this point we could start to use a bunch of React.memo wrapps or we could use useMemo hook to memoize some of the components and try to get away with some good performance, but we decided to avoid these type of performance optimizations for now as they won't help us to solve the real problem here.

We Got Hooked Bad

So it's clear to us that reusing that useNow hook was a bad idea for the App component, as this hook is changing a state every 1 second and causing a re-render of all children every 1 second!!!. I feel that I want to rename that hook from useNow to useNever. Jokes aside I wanted to discuss the problem behind this hook.

First of all this is a fictional scenario, but it's easy to see these type of mistakes happening all the time. There's some pressure to deliver the project before some launching, or developers being overloaded, and many other bad things happening that affects code on daily basis. If that could happen, how can we create a safer hook to use?

Show Your Intentions

The main issue with this hook is that the name useNow is hiding all the intentions to use that hook. The main reason to use that hook specifically is to reuse the setInterval setup and clean up, and the fact that this hook is causing a re-render in the component that uses that as a desired side-effect. We want the component to re-render every 1 second, so we use this hook.

We could have named that hook as useTimer({seconds: 1}), or useClock({seconds: 1}), or even useReRenderer({seconds: 1}) or anything else that would show the intention to use this hook for.

Flexible Options

As shown previously, using options like {seconds: 1} makes it easier for developers, in the hurry up mode, to re-use a hook, change the option and move on. Let's say that calling useTimer({seconds: 10}) in the App component is still not great, but it's definitely much better than the original re-render show.

Hook Return Relevance

Something that I noticed in the useNow hook is this hook was named after the value it returns, and this is irrelevant. Judging by the hook name, we are giving more importance to the useState value than to the useEffect in place. To understand the power that an useEffect and useLayoutEffect could have here's a list of class component life cycle callbacks that these hooks can replace:

  • componentDidMount()
  • shouldComponentUpdate()
  • componentDidUpdate()
  • componentWillUnmount()

So ignoring the effect that a hook has when naming it indicates a code smell. Furthermore I mentioned that returning now is irrelevant, it's just there for convenience.

const now = useNow();

This code is pretty much the same as:

useTimer({seconds: 1});
const now = new Date();

Wake Me Up Then

Well, let's change the paradigma by creating a new hook and at this time we are interested not to force a render periodically, but we want to force a re-render at some point in time. It's basically a "wake me up" approach. Here's the new hook:

import { useEffect, useState } from 'react';

interface Props {
  wakeMeAt: Date
};

const useAlarm = ({ wakeMeAt }: Props) => {
  const [, setNow] = useState<Date>();
  const wakeMeAtMS = wakeMeAt.getTime();

  useEffect(() => {
    const now = new Date();
    const nowMS = now.getTime();

    if (wakeMeAtMS > nowMS) {
      const ms = wakeMeAtMS - nowMS;

      const timeoutId = setTimeout(() => {
        setNow(now);
      }, ms);

      return () => {
        clearTimeout(timeoutId);
      };
    }

  }, [wakeMeAtMS]);
}

export default useAlarm;

First thing to notice is that useAlarm does not return anything as this is not important. This hook is there to wake us up at a certain date in future. To do that it plays the same trick as before by changing a state to cause a re-render, so instead of a setInterval we are setTimeout to change that only once.

Here's the new App component:

// ...

const nextMonth = () => {
  const now = new Date();

  if (now.getMonth() < 11) {
    return new Date(now.getFullYear(), now.getMonth() + 1);
  } else {
    return new Date(now.getFullYear() + 1, 0);
  }
}

const App = () => {
  const message = greetingMessage();
  useAlarm({ wakeMeAt: nextMonth() });

  return (
    <div className="App-container">
      <h1>Happy New Year</h1>
      <h3>{message}</h3>
      <div className="App-components">
        <Clock />
        <NewYearCountdown />
      </div>
      <Footer />
    </div>
  );
};

export default App;

And here's the same app with the re-render being highlighted by the Profiler:

Happy New Year with useAlarm

This is a much better scenario for us now. We are not re-rendering components like the static Footer component anymore and we didn't use any memoization for that, which is great. The only components that are re-rendering every 1 second are the Clock and the NewYearCountdown. The App component also re-renders, but it's just once per month with this new approach.

Conclusion

The React Hooks API is really cool, it's simpler, and it cuts a lot of boilerplate code that we used to write class components. I personally like it a lot. But I also acknowledge that creating custom hooks can be challenging sometimes chiefly due to useEffect and useLayoutEffect.

Also note that React.memo and useMemo are not the only way to improve performance, and using them extensively might even hide the real problems.

The lesson that we can take from this fictional journey is that a hook can be very simple to build, code-wise, but this does not mean that it's going to be easy to maintain. Naming a hook and thinking about arguments and its return is very important when designing a new one, so let's give them a bit more of thoughts. Let's understand that different developers, with different backgrounds and experience, will change the code as well, and under different conditions.

That's the end of our journey, I hope you have enjoyed this reading and somehow you can relate to some current code you are working on.

At Hashrocket, we love React and React Native! Reach out if you need help with your projects!

Photo by Roberto H on Unsplash

More posts about React