Introduction
How can we do DOM manipulations that we usually do in vanilla JavaScript? Is it even possible in React because of its declarative nature? How about performance optimization? As we all know, when a state has been changed, React will try to re-render the component, which means that it will destroy all local variables not controlled by React and re-execute them. This mental model also works for a regular function that you create, as a component is just a function. Since this is the case, wouldn’t it slow down the app if we have a very expensive calculation re-executed with each re-render?
Lesson overview
This section contains a general overview of topics that you will learn in this lesson.
- Explore
useRef
hook and its use cases. - Explain memoization and how
useCallback
anduseMemo
can be used.
The useRef hook
The useRef
hook lets you manage a value that’s not needed for rendering. They are an alternative to state, as when you want a component to “remember” some information, but you don’t want that information to trigger new renders, you can use this hook.
They are often used when performing imperative actions or accessing specific elements rendered in the DOM. Refs can also persist values throughout the component’s lifecycle, meaning that the value of the ref will not be destroyed every time a component re-renders. This is very useful when you want to store a value that you want to persist throughout the component’s lifecycle without storing it in a state.
DOM manipulation
When building web applications, sometimes you need more direct control over specific elements in the DOM. The useRef
hook comes to the rescue by providing a way to access and interact with those elements.
Imagine a button on a web page, and you want to focus on that button when the page loads. You could achieve this using the useRef
hook. Here’s how it works:
import { useRef, useEffect } from "react";
function ButtonComponent() {
const buttonRef = useRef(null);
useEffect(() => {
buttonRef.current.focus();
}, []);
return <button ref={buttonRef}>Click Me!</button>;
}
The implementation is straightforward:
- We imported
useRef
anduseEffect
in thereact
module. - We created a ref object
buttonRef
with acurrent
property initially set tonull
. Yes, passing an argument touseRef
sets the value ofcurrent
tonull
just likeuseState
. This argument is ignored in subsequent renders. - Created a
useEffect
to be executed once on the mount of the component that will try to call thefocus
method of the button element. - We’ve attached
buttonRef
to theref
attribute of the button element. This establishes the connection between thebuttonRef
and the button in the DOM.
Whenever your website loads, it will automatically focus on the button element. You might ask, how can it have the focus
method when the initial value is null
? You should by now know that rendering
and painting of the screen
comes first before React runs the useEffect
. It has already established the connection between the ref and the button before the effect is executed.
Also, remember that useRef
hook isn’t just limited to focusing elements. It can be used for various other DOM manipulation scenarios, such as scrolling to a specific position, measuring the dimensions of an element, triggering animations, and basically any DOM manipulation that you’ve done before with vanilla JavaScript. The possibilities are endless! For example, we can change the useEffect
in the above snippet to do the following. Change the button’s text, and after 2 seconds, change the text back. You should not do this and only use useRef
for non-destructive DOM operations, but just an example:
useEffect(() => {
buttonRef.current.focus();
buttonRef.current.textContent = "Hey, I'm different!";
let timeout = setTimeout(() => {
buttonRef.current.textContent = "Click Me!";
}, 2000);
return () => {
clearTimeout(timeout);
};
}, []);
The interesting thing about this is that this will never trigger a component re-render!
Another question that might pop up in your mind is, “Why not just use querySelector
or other DOM manipulation methods that we’ve done previously in vanilla JavaScript?” Dealing with the DOM ourselves defeats the purpose of using React, and wherever possible we should let React commit to the DOM itself.
We can also see that it’s similar to the useState
hook in that it can store some values. The main difference is that useRef
creates a mutable reference, allowing you to update its value without triggering a re-render. But, useState
manages an immutable state that triggers re-renders when updated.
The useMemo hook
In all of the examples, we would advise you to use the Profiler component that is provided in the react
module. If you want a more interactive alternative, use the Profiler
in the React Developer Tools. To measure rendering performance. Note that sometimes you don’t need to optimize anything because of how fast things are already. As the famous saying goes in software development:
Premature optimization is the root of all evil – The Art of Computer Programming by Donald Knuth
The useMemo
hook provides a way to add memoization inside our components. It’s used to optimize expensive or complex calculations where it caches the result of a function call and stores it to be used later without recalculating it. The memoized value is, however, recalculated only when the dependencies of the useMemo
hook change. And yes, this hook’s parameters are the same as the useEffect
hook you already know. The hook takes in two arguments: a calculateValue
callback and a dependencies
array.
Memoizing expensive calculations
In the previous Shopping Cart Project, you have some logic where you calculate the total price of the products added to the cart. You might or might not have a Cart
component that functions as a drawer, where the user can open the cart every time they either click on the Add to Cart
button or the Cart icon in the header.
An example of a Cart
component:
function Cart({ products }) {
const totalPrice = products.reduce(
(total, product) => total + product.price * product.quantity,
0
);
return (
<div>
{/* Some other content in the cart */}
{/* Products to display */}
<p>
Total Price: <strong>${totalPrice}</strong>
</p>
{/* Some button to checkout */}
</div>
);
}
In our Cart
component, we have the total price of the products calculated directly inside the component. Every time the component is rendered or updated, the calculation is performed from scratch! That doesn’t sound good… What if the user has added hundreds of thousands of products to the cart? Then it will lead to a sluggish user experience.
The reduce
method iterates over each product and performs multiplication and addition for every item in the cart. This operation becomes increasingly time-consuming as the number of products increases.
Now imagine a user who frequently opens/closes the cart. Every time the drawer is opened, the Cart
component is rendered, executing everything inside the component. This results in unnecessary recomputations of the same value even if the cart’s content hasn’t changed.
Let’s see how we can use useMemo
to address this:
import { useMemo } from "react";
function Cart({ products }) {
const totalPrice = useMemo(() => {
return products.reduce(
(total, product) => total + product.price * product.quantity,
0
);
}, [products]);
return (
<div>
{/* Some other content in the cart */}
{/* Products to display */}
<p>
Total Price: <strong>${totalPrice}</strong>
</p>
{/* Some button to checkout */}
</div>
);
}
In the example above, we can easily memoize the calculated value by wrapping it in a useMemo
, as the syntax is pretty much the same as useEffect
and almost works the same. Where useMemo
will also execute the callback on mount, and on subsequent re-renders, it will only re-execute the callback whenever one of the dependencies changes. In our case, whenever the products
prop changes.
This way, whenever a user opens/closes the cart multiple times, it will not recalculate the totalPrice
and use the cached value as long asproducts
did not change.
Referential equality checks
For this example, we will use the Profiler
component in the react
module to measure the component’s performance. We will also introduce memo
.
You do not need to start a React application for this. We’ve already got you covered a bit later, we will be sharing an interactive example, but for now, think through the code on what you think will happen, what could happen, and so on. This could also be a great exercise in reading code and visualizing how it works.
Do note that this is just a very basic example. You will encounter a lot of passing of values to other components as prop, components that are very heavy to render.
import { useState } from "react";
const ButtonComponent = ({ children, onClick }) => {
let i = 0;
let j = 0;
const ITERATION_COUNT = 10_000;
while (i < ITERATION_COUNT) {
while (j < ITERATION_COUNT) {
j += 1;
}
i += 1;
j = 0;
}
return (
<button type="button" onClick={onClick}>
{children}
</button>
);
};
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount((prevState) => prevState + 1);
};
return (
<div>
<h1>{count}</h1>
<ButtonComponent onClick={handleClick}>Click me!</ButtonComponent>
</div>
);
}
You will likely want to have a separate button component where you can handle stylings and other things in it. So we have created a component called ButtonComponent
as an example. This component takes the children
and onClick
props.
We can see that the click handler is defined in the Counter
component, and we’ve passed it to the onClick
prop of the ButtonComponent
.
We know that a component renders whenever either state changes or prop changes. Anything inside that is not controlled by React is destroyed and re-executed. Functions, variables, etc. As a result, the function handleClick
is re-created each time, and the prop onClick
of the ButtonComponent
also changes. Alright, so how can useMemo
help in here?
We already know we can memoize a value using useMemo
, right? Then we can just cache the function reference and use an empty dependency array so that it won’t change.
Let’s create a new function and name it memoizedHandleClick
:
const memoizedHandleClick = useMemo(() => handleClick, []);
We don’t need to create a new function, but this is just to test these two functions. You can also directly do the following:
// Syntax might be weird, but just remember that `useMemo` can take any value, and a function is also just a value `() => setCount((prevState) => prevState + 1)`
const handleClick = useMemo(
// first arrow function is useMemo's callback
// second arrow function is our function that will be called later, this one is going to be the cached value and what's going to be stored in `handleClick`
() => () => setCount((prevState) => prevState + 1),
[]
);
Great, useMemo
should help us here right? It shouldn’t possibly re-render the ButtonComponent
again correct? Nope, it will still re-render because whenever a component’s state
changes, it will also re-render its children, which could also be said differently - a component will re-render itself if its parent re-renders. Is there a way to fix this? Yes, there is! React in one of its APIs provides the memo wrapper function that lets you skip re-rendering a component when its props are unchanged (yes, even if the parent re-renders). We can use this memo
and wrap the ButtonComponent
in it.
import { useState, memo } from "react";
const ButtonComponent = memo(({ children, onClick }) => {
let i = 0;
let j = 0;
const ITERATION_COUNT = 10_000;
while (i < ITERATION_COUNT) {
while (j < ITERATION_COUNT) {
j += 1;
}
i += 1;
j = 0;
}
return (
<button type="button" onClick={onClick}>
{children}
</button>
);
});
Wrapping the component with a memo
prevents the downward update that is triggered above the component. So, this component will only re-render when its props
change or if its own state
changes.
With all that said and done, test and break things in our interactive example:
These are the scenarios that could happen:
- If you’ve passed
handleClick
and theButtonComponent
has amemo
. It will still re-render. Referential equality check fails (previous prop is not equal to the current prop). - If you’ve passed
memoizedHandleClick
and theButtonComponent
has amemo
. It will not re-render. Referential equality check passes (previous prop is equal to the current prop).
This works with all values that will be passed as a prop. You might see it being used frequently with the Context API:
const value = useMemo(
() => ({ someState, someFunction }),
[someState, someFunction]
);
return <Context.Provider value={value}>{children}</Context.Provider>;
The useCallback hook
The useCallback
hook provides another way to memoize a value, not just any value like useMemo
. It can only memoize a function. Did you see the previous snippet that we have with memoizing a function reference with useMemo
?
const handleClick = useMemo(
() => () => setCount((prevState) => prevState + 1),
[]
);
// or
const memoizedHandleClick = useMemo(() => handleClick, []);
With useCallback
, we don’t need to do that. It’s specifically made for functions:
import { useCallback } from "react";
// Inside a component
// Without useCallback
const handleClick = () => setCount((prevState) => prevState + 1);
// With useCallback
const handleClick = useCallback(
() => setCount((prevState) => prevState + 1),
[]
);
// or
const memoizedHandleClick = useCallback(handleClick, []);
Yay, there’s only one arrow function, and it’s simpler to read. There’s nothing extra to useCallback
other than it only memoizes functions. So the main difference between useMemo
and useCallback
is just the type of value it returns.
Which one should we use, then? Use useMemo
for any value types, and use useCallback
specifically for functions. At the end of the day, they both do similar things with a tiny difference, so use whatever you prefer.
Conclusion
Phew, this was a long lesson. Refs and memoization are difficult concepts to grasp, but we’re sure you’ll understand them with practice. Refs particularly are really useful for some use-cases, as for memoization, only reach out to it when you absolutely need it. These topics also make for great interview questions, so make sure you know the difference between useMemo
and useCallback
!
Assignment
- The article When to useMemo and useCallback by Kent C. Dodds further introduces more examples of when to use
useMemo
anduseCallback
and when you shouldn’t bother using them. - We’ve only learned about a basic implementation of the
useRef
hook. For more examples about its usage and why we should be wary of using the hook (more on the links they provided in the guide), check out the interactive guide of the React documentation for useRef hook . - The article useRef instead of querySelector in React by Caleb Olojo briefly tells some unexpected behaviors when trying to manipulate the DOM directly with DOM manipulation methods and why we should prefer
useRef
over other DOM manipulation methods likequerySelector
. Check it out! - As we have learned, the
useRef
hook has other uses other than what we’ve primarily covered which is DOM Manipulation. Get to know more about its use-cases in this great article by Dan Abramov Making setInterval Declarative with React Hooks.
Knowledge check
The following questions are an opportunity to reflect on key topics in this lesson. If you can’t answer a question, click on it to review the material, but keep in mind you are not expected to memorize or master this knowledge.
Additional resources
This section contains helpful links to related content. It isn’t required, so consider it supplemental.
- It looks like this lesson doesn’t have any additional resources yet. Help us expand this section by contributing to our curriculum.