A Thoughtful Way To Use React’s useRef()
Hook
useState
and useReducer
can cause your component to re-render each time there is a call to the update functions. In this article, you will find out how to use the useRef()
hook to keep track of variables without causing re-renders, and how to enforce the re-rendering of React Components.In React components, there are times when frequent changes have to be tracked without enforcing the re-rendering of the component. It can also be that there is a need to re-render the component efficiently. While useState
and useReducer
hooks are the React API to manage local state in a React component, they can also come at a cost of being called too often making the component to re-render for each call made to the update functions.
In this article, I’ll explain why useState
is not efficient for tracking some states, illustrate how useState
creates too much re-render of a component, how values that are stored in a variable are not persisted in a component, and last but not least, how useRef
can be used keep track of variables without causing re-render of the component. And give a solution on how to enforce re-render without affecting the performance of a component.
After the evolution of functional components, functional components got the ability to have a local state that causes re-rendering of the component once there is an update to any of their local state.
function Card (props) {
const [toggled, setToggled] = useState(false);
const handleToggleBody = () => {
setToggled(!toggled)
}
return (<section className="card">
<h3 className="card__title" onMouseMove={handleToggleBody}>
{props.title}
</h3>
{toggled && <article className="card__body">
{props.body}
</article>}
</section>)
}
// Consumed as:
<Card name="something" body="very very interesting" />
In the component above, a card is rendered using a section
element having a child h3
with a card__title
class which holds the title of the card, the body of the card is rendered in an article tag with the body of card__body
. We rely on the title
and body
from the props to set the content of the title and body of the card, while the body is only toggled when the header is hovered over.
Re-rendering A Component With useState
Initial rendering of a component is done when a component has its pristine, undiluted state values, just like the Card component, its initial render is when the mouseover event is yet to be triggered. Re-rendering of a component is done in a component when one of its local states or props have been updated, this causes the component to call its render method to display the latest elements based on the state update.
In the Card
component, the mousemove
event handler calls the handleToggleBody
function to update the state negating the previous value of the toggled state.
We can see this scenario in the handleToggleBody
function calling the setToggled
state update function. This causes the function to be called every time the event is triggered.
Storing State Values In A Variable
A workaround for the repeated re-rendering is using a local variable within the component to hold the toggled state which can also be updated to prevent the frequent re-rendering — which is carried out only when there is an update to local states or props of a component.
function Card (props) {
let toggled = false;
const handleToggleBody = () => {
toggled = !toggled;
console.log(toggled);
}
return (<section className="card">
<section className="cardTitle" onMouseMove={handleToggleBody}>
{title}
</section>
{toggled && <article className="cardBody">
{body}
</article>}
</section>)
}
<Card name="something" body="very very interesting" />
This comes with an unexpected behavior where the value is updated but the component is not re-rendered because no internal state or props has changed to trigger a re-render of the component.
Local Variables Are Not Persisted Across Rerender
Let’s consider the steps from initial rendering to a re-rendering of a React component.
- Initially, the component initializes all variables to the default values, also stores all the state and refs to a unique store as defined by the React algorithm.
- When a new update is available for the component through an update to its props or state, React pulls the old value for states and refs from its store and re-initializes the state to the old value also applying an update to the states and refs that have an update.
- It then runs the function for the component to render the component with the updated states and refs. This re-rendering will also re-initialize variables to hold their initial values as defined in the component since they are not tracked.
- The component is then re-rendered.
Below is an example that can illustrate this:
function Card (props) {
let toggled = false;
const handleToggleBody = () => {
toggled = true;
console.log(toggled);
};
useEffect(() => {
console.log(“Component rendered, the value of toggled is:“, toggled);
}, [props.title]);
return (
<section className=“card”>
<h3 className=“card__title” onMouseMove={handleToggleBody}>
{props.title}
</h3>
{toggled && <article className=“card__body”>{props.body}</article>}
</section>
);
}
// Renders the application
function App () {
const [cardDetails, setCardDetails] = useState({
title: “Something”,
body: “uniquely done”,
});
useEffect(() => {
setTimeout(() => {
setCardDetails({
title: “We”,
body: “have updated something nice”,
});
}, 5000); // Force an update after 5s
}, []);
return (
<div>
<Card title={cardDetails.title} body={cardDetails.body} />
</div>
);
}
In the above code, the Card
component is being rendered as a child in the App
component. The App
component is relying on an internal state object named cardDetails
to store the details of the card. Also, the component makes an update to the cardDetails
state after 5seconds of initial rendering to force a re-rendering of the Card
component list.
The Card
has a slight behavior; instead of switching the toggled state, it is set to true
when a mouse cursor is placed on the title of the card. Also, a useEffect
hook is used to track the value of the toggled
variable after re-rendering.
The result after running this code and placing a mouse on the title updates the variable internally but does not cause a re-render, meanwhile, a re-render is triggered by the parent component which re-initializes the variable to the initial state of false
as defined in the component. Interesting!
About useRef()
Hook
Accessing DOM elements is core JavaScript in the browser, using vanilla JavaScript a div
element with class "title"
can be accessed using:
<div class="title">
This is a title of a div
</div>
<script>
const titleDiv = document.querySelector("div.title")
</script>
The reference to the element can be used to do interesting things such as changing the text content titleDiv.textContent = "this is a newer title"
or changing the class name titleDiv.classList = "This is the class"
and much more operations.
Overtime, DOM manipulation libraries like jQuery made this process seamless with a single function call using the $
sign. Getting the same element using jQuery is possible through const el = ("div.title")
, also the text content can be updated through the jQuery’s API: el.text("New text for the title div")
.
Refs In React Through The useRef
Hook
ReactJS being a modern frontend library took it further by providing a Ref API to access its element, and even a step further through the useRef
hook for a functional component.
import React, {useRef, useEffect} from "react";
export default function (props) {
// Initialized a hook to hold the reference to the title div.
const titleRef = useRef();
useEffect(function () {
setTimeout(() => {
titleRef.current.textContent = "Updated Text"
}, 2000); // Update the content of the element after 2seconds
}, []);
return <div className="container">
{/** The reference to the element happens here **/ }
<div className="title" ref={titleRef}>Original title</div>
</div>
}
As seen above, after the 2 seconds of the component initial rendering, the text content for the div
element with the className of title changes to “Updated text”.
How Values Are Stored In useRef
A Ref variable in React is a mutable object, but the value is persisted by React across re-renders. A ref object has a single property named current
making refs have a structure similar to { current: ReactElementReference }
.
The decision by the React Team to make refs persistent and mutable should be seen as a wise one. For example, during the re-rendering of a component, the DOM element may get updated during the process, then it is necessary for the ref to the DOM element to be updated too, and if not updated, the reference should be maintained. This helps to avoid inconsistencies in the final rendering.
Explicitly Updating The Value Of A useRef
Variable
The update to a useRef
variable, the new value can be assigned to the .current
of a ref variable. This should be done with caution when a ref variable is referencing a DOM element which can cause some unexpected behavior, aside from this, updating a ref variable is safe.
function User() {
const name = useRef("Aleem");
useEffect(() => {
setTimeout(() => {
name.current = "Isiaka";
console.log(name);
}, 5000);
});
return <div>{name.current}</div>;
}
Storing Values In useRef
A unique way to implement a useRef
hook is to use it to store values instead of DOM references. These values can either be a state that does not need to change too often or a state that should change as frequently as possible but should not trigger full re-rendering of the component.
Bringing back the card example, instead of storing values as a state, or a variable, a ref is used instead.
function Card (props) {
let toggled = useRef(false);
const handleToggleBody = () => {
toggled.current = !toggled.current;
}
return (
<section className=“card”>
<h3 className=“card__title” onMouseMove={handleToggleBody}>
{props.title}
</h3>
{toggled && <article className=“card__body”>{props.body}</article>}
</section>
);
</section>)
}
This code gives the desired result internally but not visually. The value of the toggled state is persisted but no re-rendering is done when the update is done, this is because refs are expected to hold the same values throughout the lifecycle of a component, React does not expect them to change.
Shallow And Deep Rerender
In React, there are two rendering mechanisms, shallow and deep rendering. Shallow rendering affects just the component and not the children, while deep rendering affects the component itself and all of its children.
When an update is made to a ref, the shallow rendering mechanism is used to re-render the component.
function UserAvatar (props) {
return <img src={props.src} />
}
function Username (props) {
return <span>{props.name}</span>
}
function User () {
const user = useRef({
name: "Aleem Isiaka",
avatarURL: "https://icotar.com/avatar/jake.png?bg=e91e63",
})
console.log("Original Name", user.current.name);
console.log("Original Avatar URL", user.current.avatarURL);
useEffect(() => {
setTimeout(() => {
user.current = {
name: "Isiaka Aleem",
avatarURL: "https://icotar.com/avatar/craig.png?s=50", // a new image
};
},5000)
})
// Both children won't be re-rendered due to shallow rendering mechanism
// implemented for useRef
return (<div>
<Username name={user.name} />
<UserAvatar src={user.avatarURL} />
</div>);
}
In the above example, the user’s details are stored in a ref which is updated after 5 seconds, the User component has two children, Username to display the user’s name and UserAvatar
to display the user’s avatar image.
After the update has been made, the value of the useRef
is updated but the children are not updating their UI since they are not re-rendered. This is shallow re-rendering, and it is what is implemented for useRef hook.
Deep re-rendering is used when an update is carried out on a state using the useState
hook or an update to the component’s props.
function UserAvatar (props) {
return <img src={props.src} />
}
function Username (props) {
return <span>{props.name}</span>
}
function User () {
const [user, setUser] = useState({
name: "Aleem Isiaka",
avatarURL: "https://icotar.com/avatar/jake.png?bg=e91e63",
});
useEffect(() => {
setTimeout(() => {
setUser({
name: "Isiaka Aleem",
avatarURL: "https://icotar.com/avatar/craig.png?s=50", // a new image
});
},5000);
})
// Both children are re-rendered due to deep rendering mechanism
// implemented for useState hook
return (<div>
<Username name={user.name} />
<UserAvatar src={user.avatarURL} />
</div>);
}
Contrary to the result experienced when useRef
is used, the children, in this case, get the latest value and are re-rendered making their UIs have the desired effects.
Forcing A Deep Re-render For useRef
Update
To achieve a deep re-render when an update is made to refs, the deep re-rendering mechanism of the useState
hook can be partially implemented.
function UserAvatar (props) {
return <img src={props.src} />
}
function Username (props) {
return <span>{props.name}</span>
}
function User () {
const user = useRef({
name: "Aleem Isiaka",
avatarURL: "https://icotar.com/avatar/jake.png?bg=e91e63",
})
const [, setForceUpdate] = useState(Date.now());
useEffect(() => {
setTimeout(() => {
user.current = {
name: "Isiaka Aleem",
avatarURL: "https://icotar.com/avatar/craig.png?s=50", // a new image
};
setForceUpdate();
},5000)
})
return (<div>
<Username name={user.name} />
<UserAvatar src={user.avatarURL} />
</div>);
}
In the above improvement to the User
component, a state is introduced but its value is ignored since it is not required, while the update function to enforce a rerender of the component is named setForceUpdate
to maintain the naming convention for useState
hook. The component behaves as expected and re-renders the children after the ref has been updated.
This can raise questions such as:
“Is this not an anti-pattern?”
or
“Is this not doing the same thing as the initial problem but differently?”
Sure, this is an anti-pattern, because we are taking advantage of the flexibility of useRef
hook to store local states, and still calling useState
hook to ensure the children get the latest value of the useRef
variable current value both of which can be achieved with useState
.
Yes, this is doing almost the same thing as the initial case but differently. The setForceUpdate
function does a deep re-rendering but does not update any state that is acting on the component’s element, which keeps it consistent across re-render.
Conclusion
Frequently updating state in a React component using useState
hook can cause undesired effects. We have also seen while variables can be a go-to option; they are not persisted across the re-render of a component like a state is persisted.
Refs in React are used to store a reference to a React element and their values are persisted across re-render. Refs are mutable objects, hence they can be updated explicitly and can hold values other than a reference to a React element.
Storing values in refs solve the problem of frequent re-rendering but brought a new challenge of the component not being updated after a ref’s value has changed which can be solved by introducing a setForceUpdate
state update function.
Overall, the takeaways here are:
- We can store values in refs and have them updated, which is more efficient than
useState
which can be expensive when the values are to be updated multiple times within a second. - We can force React to re-render a component, even when there is no need for the update by using a non-reference
useState
update function. - We can combine 1 and 2 to have a high-performance ever-changing component.
References
- “Hooks API Reference,” React Docs
- “Understanding
useRef
: An Intro To Refs And React Hooks,” Kris Mason, Medium - “Managing Component State With The
useRef
Hook,” React Hooks in Action (Chapter 6), Manning Publications Co. - “Use
useRef
Hook To Store Values You Want To Keep An Eye On,” Marios Fakiolas
Further Reading
- Exploring The Potential Of Web Workers For Multithreading On The Web
- The Safest Way To Hide Your API Keys When Using React
- SVG Coding Examples: Useful Recipes For Writing Vectors By Hand
- Best Practices With React Hooks