React 16 was a great and much-needed addition to the React framework. The new version introduced the Hooks API, which allows developers to use state and other class component functionality on function components.
I recall reading up on the React Hooks documentation and understanding the intent behind most of the native hooks such as:
- useState: extend component level state management to functional components;
- useEffect: allow side effect code to be able to run on functional components at different stages of their lifecycle
- useContext: enable functional components to access context data.
The intent of some of these hooks, however, was not so clear to me right away. One of these hooks is `useMemo`. The React documentation for useMemo states the following:
"Returns a memoized value. Pass a “create” function and an array of dependencies. useMemo will only recompute the memoized value when one of the dependencies has changed. This optimization helps to avoid expensive calculations on every render."
useMemo is a hook that helps optimize functional components by memoizing a computed value. The docs do not provide a concrete example on how exactly the useMemo hook is meant to be used or how exactly memoization works on a functional component. This post aims to complement the useMemo React docs with a concrete example and use case to help other developers understand useMemo more thoroughly.
Our toy use case will be a fruit web app that, when given a basket, displays the fruits within that basket. We are also able to render the next fruit basket as well as toggle a black border that wraps the fruits in the list. The app would work like this:
Let us start by defining our root app component, `MyFruitsApp`.
This component basically defines what are the unique fruits data available, whether they should be displayed with a black border and what are the available baskets and their respective content.
MyFruitBasket is a component meant to store which basket we are currently looking at and render fruit information for each fruit on the basket on focus into the DOM.
Now, here is the interesting thing about how this app is set up. The fruit objects in the baskets array do not contain the entire fruit information, just their names. If we want to access the fruit information, we have to look at the uniqueFruits hashmap. We only want to get the data for the fruits in the current basket being displayed in `MyFruitBasket`. So while we are rendering this component, we perform some basic computation, mapping the array for the currently selected basket into a fruitObjectArray containing the fruit objects with the data we need.
So far so good! Our beautiful fruits app works wonderfully well and displays exactly as shown in figure 1. I have intentionally added a log statement into the loop where we filter our uniqueFruits array according to which fruits we have on our current basket. If we look into the console after the first render, we can see the logs:
We go over the 2 fruit names in the currently selected fruit basket getting their respective objects in from the uniqueFruits map. The time complexity for this operation is O(n) where n is the total length of fruit names inside the selected basket array. This linear-complexity operation could get expensive if we ever need to scale our app to hold large lists of fruits in our fruit baskets. For the scope of this blog post, we will try to optimize computation for this operation assuming we will scale basket sizes in the future.
Since this operation will get expensive, we would rather do it only when strictly necessary, i.e. when MyFruitsBasket state `selectedBasket` or the map/array props change. Unfortunately, React components re-render every time a prop, state, or context value changes. Let’s see what happens when we toggle the black border flag by clicking on the border toggle button:
As expected, our black border variable toggles on the parent component's state. This changes the prop being passed over to `MyFruitsBasket` and triggers a re-render. Since we are re-rendering our basket component, the function executes again and we have to calculate the fruitsObjectArray all over again even though the border flag is unrelated to the filtering calculation! What to do here? Are we bound to run this unnecessary calculation every render?
useMemo to the rescue!
As stated in the React documentation at the beginning of the post, this is where the hook is meant to shine. Let’s make a slight refactor to our `MyFruitBasket` component to leverage the useMemo hook.
As we observed previously, we only need to execute the expensive filtering operation when either the uniqueFruits/fruitBaskets props change or when we choose to select a new basket to look at by changing selectedBasket state. Let’s observe how our components behave if we toggle the border flag now:
The useMemo hook prevented our calculation of the filtered fruits object array during the border re-render. This was possible because we did the calculation on the first render and none of the dependencies given to useMemo had their references changed to justify the trigger of a new recalculation. useMemo caches the value of `fruitObjectArray` until one of its dependencies change. This optimizes performance on our functional component!
A few Caveats
As we just illustrated, useMemo can be an awesome Hook when we are really focusing on optimizing app performance by preventing unnecessary expensive calculations on every render. But it needs some level of caution, here are a couple of caveats to avoid weird bugs in the future.
Do not rely on useMemo for semantic guarantee
As clear on their documentation, the React development team reserves the right to flush the cached value on useMemo hooks from time to time besides no reference changes in the dependency array for internal library performance reasons. This means that if you build logic that relies on consistent refreshing behavior for memoized values (only when dependencies change), React may disrupt this expected behavior at its own discretion. So avoid writing code that relies on timely changes to memoized values.
Make sure to get the dependency array right
useMemo allows you to define exactly what value changes should trigger a refresh in the expensive value. This means it is our responsibility as developers to exhaustively add all relevant dependencies that should trigger a refresh of an expensive value. Luckily the React team has a lint rule recommendation to avoid bugs when declaring the dependencies in useMemo.
My hope is that developers will find this illustrative example useful when trying to understand hooks like useMemo and useCallback in the future.
Happy coding :)