React
React Performance with Hooks
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][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:
- 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 twosetIntervals
will not be synced together. - 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][happy-new-year-rerender-1]
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][happy-new-year-rerender-2]
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
[happy-new-year-app]: https://user-images.githubusercontent.com/317960/103158721-e9e59f00-478e-11eb-84f6-0c6ff187df3d.png "Happy New Year App" [happy-new-year-rerender-1]: https://user-images.githubusercontent.com/317960/103228796-9f376480-48ff-11eb-8c27-ab7cdc5ed6b6.gif "Happy New Year Re-Render" [happy-new-year-rerender-2]: https://user-images.githubusercontent.com/317960/103233739-d0b62d00-490b-11eb-8089-884886870ebd.gif "Happy New Year with useAlarm"