This Ain’t Your Grandma’s React (Part 3)
This is the third of four articles devoted to banishing the idea that you can look at functional components using Hooks as if they were like class-based components. The other articles are:
- This Ain’t Your Grandmas’s React: How Hooks Rewrote Your Understanding of Components
- This Ain’t Your Grandma’s React II: How State Works Without Classes
- This Ain’t Your Grandma’s React IV: How Hooks Make Side Effects Make Sense
If you haven’t already read the previous two, you may want to take some time and do so.
This article looks at how functional components let you consistently use variables after rendering is complete. It also discusses how to freeze and update information, and gives you the groundwork for side effects in Part Four.
Let’s take one more look at the diagram I created showing how Hooks plug into the single function that comprises a functional component:
In Part One, we talked about how the overall Hooks architecture differs from classes. In Part Two, we talked about how state is managed, focusing on the Hooks on the left side of this diagram. In this article, we’ll focus on what makes the Hooks on the right side of the diagram work, and go into the first two.
Closures Are King
There’s two huge elephants in the corner of the functional component room: closures and dependencies. We touched on closures in Part One. Closures freeze values at the moment they’re created. Class-based components don’t do that. This is one of the biggest stumbling blocks developers have when they attempt to switch from classes to Hooks.
With classes, when a function is called, it reflects the state of the instance at the moment the function was called. With functional components, when a Hook is called, its function reflects the state of the instance at the moment the component was rendered. It doesn’t matter how much later the Hook’s function is used. It only knows what it knew then.
This helps reduce bugs where the instance’s fields change out from under a side effect. As an example, take a look at the following class-based component:
Here we create two buttons, one to set the instance’s
name property, one to read it. But because
setName is available outside of the component’s lifecycle, any other component can change it outside of the render phase. A user clicking
Who am I? will see the last value given to
name, not necessarily the last value they provided when they clicked
With a functional component, the closure makes sure that the value is frozen into the render method:
In this case,
Name me updates the state of the component, and causes the component to rerender with the new value. Every place where you see
name in this method, you know that it has the value that was last available when the component was rendered.
Every Hook in blue on the right —
useLayoutEffect — looks like it takes a function as its first argument. It isn’t just a function. That function becomes a closure the moment the Hook is called. Every time you call one of those Hooks, you create a closure with the information frozen from that render cycle.
So, if the information is frozen, how do you update the Hook’s closure with new information?
Dependencies Hold the Keys to the Kingdom
The second argument in these Hooks is an array of constants to watch. If any of the constants change from one render to the next, it tells the Hook that it needs to create a new closure using the function that was provided to it.
Let’s take a look at another example from Part One:
useCallback, a closure is created where the value of
id is frozen in the async function.
id is also referenced in the dependency array following the function. When the main function is called with a new
id value, a new closure is created, capturing that new value of
useEffect makes a closure that freezes the reference to
fetchData appears in the dependency array, so that when it changes, the closure in
useEffect is updated.
Closures are frozen. Dependencies aren’t.
This may feel subtle, but it can be a cause of bugs if not considered. The function might look like it’s accessing the current state and props, because it refers to them. But it is frozen in time when the values in the dependency array changes. Take the following polling component:
We have an effect that polls the server over a provided interval, updating whether or not the user is logged in and who they are. If their logged-in state changes, the dependency on line 18 tells the effect to update with the new version of
isLoggedIn. This clears the polling interval and starts a new one with the new logged in state value.
interval change, however, those dependencies aren’t tracked, so the closure isn’t updated. As long as
isLoggedIn stays the same, the values for
interval can change all they want outside the Hook, but the closure is still frozen with the old values for
interval. Only when
isLoggedIn changes does the function update with the new
The dependencies track every render. The closure does not.
react-hooks/exhaustive-deps ESLint warning helps us keep track of when we aren’t tracking all of the external variables we’ve pulled into these closures.
Empty dependency arrays
Sometimes you’ll see a Hook that takes an empty dependency array, like this:
When a Hook takes an empty dependency array, it tells React that this closure will never change. The function is run once, and the returned value is frozen until the component is unmounted. This can be valuable for creating default object literals like the one shown above, or for creating one-time side effects that only run when the component is first mounted. (We’ll discuss side effect creation in Part Four, when we get to
This is different from calling a Hook with no dependency array at all. That tells React to update the closure every single time, even if it isn’t necessary. This can be quite expensive and is rarely useful. Even if you want a side effect to rerun on every render, you can find a less expensive way to do this, like tracking change events, having a side effect create a MutationObserver, or create an interval-based polling mechanism.
Never Ignore react-hooks/exhaustive-deps.
How many times have you seen that annoying warning and decided to stick an
// eslint-disable-line comment on it and call it a day?
When React complains about you not checking your dependencies properly, it’s telling you one of two things:
- Your closure will become stale and have old data in it.
- Your dependencies aren’t actually related to your closure.
In the first case, there’s probably another variable you’ve forgotten to track. In the second case, you’re asking React to update something, not because the update impacts the closure, but because the dependency is hiding a piece of business logic.
If a dependency for
foo updates a closure based on
bar, there’s naturally going to be a question of why the two are connected. If it’s not obvious why the dependency is associated with the closure, there’s probably some fragility baked into the code, some magic happening somewhere else that would “make this all make sense” — if you happened to know what was happening in the grandfather component.
By keeping your dependencies localized and transparent, you encapsulate your logic and dependencies into a single unit, making it more understandable for the next person who has to read your code.
Alright, now that we’ve looked at how closures and dependencies work together, let’s look at the Hooks that take advantage of this to reduce rendering and more clearly identify data relationships.
useMemo: Reduce Calculation Time, Define Dependencies, Fix Object Literal References
useMemo() has three basic purposes. First, the docs say it lets you save a value for future use, so you only have to calculate it once and reuse it many times. This can be valuable for information that is expensive to calculate, such as a collection of JSX that only changes when specific data points change.
To be honest, I haven’t found a lot of use cases for that. What I’ve found useful, however, are two other uses: reducing the complexity of dependencies, and keeping references to an object literal from changing.
useMemo clearly defines what its result depends on, with its dependency array. This can be useful to simplify dependency arrays in other Hooks:
In this component,
folderData is only updated if
roomId change. In turn,
useEffect only updates when
By splitting dependencies in this way, we can reduce the amount we need to keep track of in a specific circumstance. We can look at the object created by
useMemo and know it is dependent on
roomId, but not care about any other values used by this component. Likewise, the
useEffect call shows us we care when
folderData changes, but we don’t have to care about why it changes. We just need to know an update happens when
useMemo is also useful for preventing infinite renders when saving object literals to state:
In this case, line 4 creates a new object literal every time, forcing
setTuple to rerender every time. State setters force a rerender if it gets a new object reference. If you memoize the literal, the state setter will recognize when you give it the exact same object, and it won’t rerender if it does. It will only rerender when it gets a new object, which will only happen when a dependency changes.
useCallback: useMemo for functions
useCallback is simply a shorthand for returning a function from a
While you can use
useMemo for functions,
useCallback highlights the act of creating a closure for a function instead of a value. It can be used to simplify dependency chains, the same way that
useMemo can. This was illustrated in the Dependencies section’s first example above.
useCallback can also be used to give other components the option of not rerendering. When a function memoized with
useCallback is sent to another component as a property, that component can compare it to the previous version of the property, see that it’s the same, and choose to not rerender if nothing else changed:
The empty dependency arrays in the example tell React to build once and never again. This can be really useful to keep other dependencies from updating. In this case, because the inputs’
onChange handlers will never change, the form will only rerender an input if its value changes.
While this is a pretty trivial optimization for an input field, if the component is, say, output for a database with methods for making updates, rerendering the entire database every time can be painful.
Beware Premature Optimization
A lot of
useCallback’s benefits come from reducing the frequency of rerenders and updates. They can come at a price, though. They can make code unnecessarily more complex. Like any optimization, they may not be useful, until they are.
useCallback show their everyday value is in chunking code into smaller, dependent pieces, and preventing infinite renders. You can use them to make dependency trees, reducing the number of variables you need to track in place by consolidating them in different Hooks. You can also use them to keep object literal creation from triggering unnecessary updates. But beyond that, wait and see what your browser experience shows you before going deeper.
Alright, so this was a bunch of concepts. The main things to take away include:
- Functional components use closures to keep values consistent with what’s being rendered, regardless of when those values are eventually used. This helps keep values from changing out from under you, which can happen more easily in class-based components.
- Dependencies keep you from recreating the same data or rerunning the same code when nothing has changed.
useMemohelps you prevent infinite renders and lets you consolidate dependencies.
useCallbackadds to what
useMemodoes by giving child components the opportunity to optimize for unchanged functions.
Alright, that’s it for the memoization hooks. In Part Four, we’ll take a look at what sets functional components far apart from class-based components: side effect management.