In my previous article, I talked about React state. Now it's time to discuss about React reference:
- What is it?
- How to use them?
- When to use it?
- How does it work under the hood?
Let's go.
What is it?
A React reference is simply an object that has it's reference which is fixed during component renders and that that a key current
that is mutated.
Unlike React state, when we change a reference (mutates it) React WILL NOT trigger a re-render of the component.
How to use it?
Before version 16.8.6
of React it was possible only to use ref on class component.
Class component
To create a reference in a Class component you only have to call:
import React from 'react';
const ref = React.createRef();
Call it in:
- the constructor:
class MyClassComponent extends React.Component {
constructor() {
this.myRef = React.createRef();
}
render() {
return <p>A simple class component with a ref</p>;
}
}
- directly declaring the property name you want:
class MyClassComponent extends React.Component {
myRef = React.createRef();
render() {
return <p>A simple class component with a state</p>;
}
}
Note: You can use both as you wish. It's the same. But constructor will give you the possibility to use props to initialize the ref:
class MyClassComponent extends React.Component {
constructor(props) {
this.myRef = React.createRef();
this.myRef.current = props.someValue;
}
render() {
return <p>A simple class component with a ref</p>;
}
}
Functional component
After 16.8.6
, hooks have been introduced, especially useRef
:
import { useRef } from 'react';
const ref = useRef(initValue);
With a component you will have:
import { useRef } from "react";
function StateFunctionalComponent() {
// myRef will have a fixed reference
// The initial value is 0
const myRef = useRef(0);
return <p>Functional component with state</p>;
}
Access and update
Then, once you have created the reference, you probably want to get the value and update it.
You will just work with the current
property:
const myRef = useRef();
// Get the value
console.log('The value is:', myRef.current);
// Update the value
myRef.current = 'New value';
Warning: Do not update the reference in the render directly. You will see in the next part where to do it and when it is useful.
What should not be done with ref?
I spoiled it a little at the end of the previous part, you should never update/read a reference inside the render directly, the only exception is for lazy initialization.
What is lazy initialization?
Lazy init is when you check if the ref has not value to set one. It's useful for example when you work with Portal to get the container:
function MyComponent() {
const container = useRef();
if (!container) {
container.current =
document.getElementById("myContainer");
}
return ReactDOM.createPortal(
<p>Will be inside the element with id: myContainer</p>,
container.current
);
}
Note: If you do not know what a Portal is, here is the
React
definition:Portals provide a first-class way to render children into a DOM node that exists outside the DOM hierarchy of the parent component.
Why should you not update/read in render?
It's because of incoming concurrent rendering. With concurrent mode, the rendering process will not be synchronous anymore, so it will be possible that rendering of some component is "paused" to keep as most as possible 60 frames per second and a nice interactivity feeling.
So it would be possible to make inconcistency if a ref is used inside render for UI (because we mutate an object).
Whereas React
will ensure there is no inconcistency with React states.
To help you to identify where are problems with ref
, there will be some warning in the console about that. You can see this PR: useRef: Warn about reading or writing mutable values during render that introduce the warnings.
When to use it?
Okay now that we know what is it and that the component will not re-render after mutation of the reference, when is it useful?
There is multiple cases, let's see them.
Note: In all the example I will code with Functional component but you can do the same with Class component
Get reference to DOM element
The main role of reference is to have access to a DOM element and then be able to do some process on the element like: focus, get the value of an input, ...
In this case, you have to put the ref on the "React DOM element".
function MyComponent() {
const inputRef = useRef();
return <input type="text" ref={inputRef} />;
}
Then you have access to the real DOM element through ref.current
.
For example, with the input we can get the value filled by the user:
function MyComponent() {
const inputRef = useRef();
return (
<div>
<input type="text" ref={inputRef} />
<button
type="button"
onClick={() =>
console.log(
"The value is:",
inputRef.current.value
)
}
>
Show the value
</button>
</div>
);
}
Note: Get element with
useRef
if the element to select is here at the mount.If the element is conditionally rendered, you would probably prefer to use a ref callback
function MyComponent() {
const [show, setShow] = useState(false);
const refCallback = useCallback((node) => {
if (!node) {
console.log("The node is unmounted");
} else {
console.log("The node is", node);
}
}, []);
return (
<div>
<button
type="button"
onClick={() => setShow((prev) => !prev)}
>
Show / unshow
</button>
{show && (
<div ref={refCallback}>
Element with ref callback
</div>
)}
</div>
);
}
Note: You can see that I
useCallback
the ref callback otherwise the method will called at each render that will be bad for the behavior's consistency. I you want other cases where to useuseCallback
, you can see my article When to use useCallback.Note: If you want to pass the
ref
to a component you will have to choice with FC: pass it with different name thanref
orforwardRef
on the component. In CC you will have to name it differently.
// Forward the ref
const FunctionalComponent = React.forwardRef(
(props, ref) => {
// Content of component
}
);
// Different name
function FunctionalComponent({ customRef }) {
// Content of component
}
Store data not useful for UI (used in event listener for example)
One other case is to store value that does not need to trigger a re-render, for example when you use it only in event listener.
Let's take the example where you want to prevent clicking on a button (but not show a different style), in this case let's use a ref
:
function MyComponent() {
const preventClick = useRef(false);
return (
<div>
<button
type="button"
onClick={() =>
(preventClick.current = !preventClick.current)
}
>
Enable / Disable click
</button>
<button
type="button"
onClick={() => {
if (preventClick.current) {
return;
}
console.log("You are able to click");
}}
>
Will you be able to click?
</button>
</div>
);
}
Note: In reality, we would probably show a different style which will make us use a
state
instead.
Get latest value of a value in useCallback
Sometimes I do not want to useCallback
some function for example when doing memoization for performances.
For example:
const callback = useCallback(() => {
console.log("I use the dep:", value);
}, [value]);
This callback will be recreated, each time value
is changing. But most of the time I do not want that. For example when the callback is used as an event handler.
So in this case, I will put the value
in a ref
that will ensure me to get the latest value of the value
without recreating a new callback.
const valueRef = useRef(value);
useEffect(() => {
// I don't care that it's executed at each render
// because I want always the latest value
// I save a check on the dependency
valueRef.current = value;
});
const reallyStableCallback = useCallback(() => {
console.log("I use the dep:", valueRef.current);
}, []);
Warning: Do not forget to update the reference! Otherwise, you will not have the latest
value
.
Count the number of render
You can easily store the number of render thanks to a ref
combined with useEffect
:
function MyComponent() {
const renderCount = useRef(1);
useEffect(() => {
renderCount.current++;
});
return <p>Number of render: {renderCount.current}</p>;
}
Know if a component has been already mounted
function MyComponent() {
const isMounted = useRef(false);
const [count, setCount] = useState(0);
useEffect(() => {
if (isMounted.current) {
console.log("The count has changed to:", count);
}
}, [count]);
useEffect(() => {
isMounted.current = true;
}, []);
return (
<button
type="button"
onClick={() => setCount((prev) => prev + 1)}
>
Inc count: {count}
</button>
);
}
Warning: When using this pattern, to know if the component is mounted in a
useEffect
then the order ofuseEffect
is really important. The one that updates theisMounted
MUST be the last one.
Keep a previous value
Another use case is when you want to keep the value of a state during the previous render. It can be useful when you compare to the current one in a useEffect
to know if it is one of the dependency that has changed.
function MyComponent() {
const [otherState, setOtherState] = useState(0);
const [count, setCount] = useState(0);
const previousCount = useRef(count);
useEffect(() => {
if (previousCount.current !== count) {
console.log(
"The count has changed during this render " +
"(maybe otherState too)"
);
} else {
console.log(
"It's sure that otherState has changed " +
"during this render"
);
}
}, [count, otherState]);
useEffect(() => {
previousCount.current = count;
}, [count]);
return (
<div>
<button
type="button"
onClick={() => setCount((prev) => prev + 1)}
>
Inc count: {count}
</button>
<button
type="button"
onClick={() => setOtherState((prev) => prev + 1)}
>
Inc otherState: {otherState}
</button>
<button
type="button"
onClick={() => {
setCount((prev) => prev + 1);
setOtherState((prev) => prev + 1);
}}
>
Inc both
</button>
</div>
);
}
Note: It's not because the
count
has changed during the render than theotherState
did not changed too. It can happen becauseReact
batches update in that case.
How React
assigns the DOM node to a ref?
Previously we have seen than the main use case is to get a reference to a DOM node. But how does React do it under the hood?
One thing you should understand is the difference of execution between useEffect
and useLayoutEffect
: layoutEffect
s are executed synchronously after the rendering phase contrary to effect
s that are executed asynchronously (they are just schedule but not ensure to be executed directly).
At the first rendering, React will transform React elements into Fiber nodes.
Basically, during the rendering, React will process from the Root node until the deepest component. Then it will go up in the component tree.
Note: Here I have a simple tree with no siblings component. When there are siblings it processes a branch, complete the work and go on the other branch.
Begin work phase:
When processing a node, from top to bottom, React can detect when a node is a HostComponent (i.e. div
, p
, ... native DOM tag) and has a prop ref
assign to it.
If it's the case, React will flag this node and put on the fiber node a ref
key containing the reference to the ref
(which is basically an object with a current
key as we have seen earlier).
Complete work phase:
Then, when React has reached the last child it will go up in the tree, it's at this moment that the previously setted flag has an effect. It will tell to the parent fiber node:
HostComponent: "Hey, someone wants my DOM node, put me in you (fiber node) as a
firstEffect
that will be executed before alllayoutEffect
you have :)"
Then the parent fiber node tells to its parent:
HostComponent parent: "Hey, we told me that this fiber node is my
firstEffect
it needs to be yours too. But I have somelayoutEffect
can you execute me next, please?"
And this discussion happens to each fiber node until we get back to the Root fiber node.
Then the Root fiber node has just to execute its firstEffect
.
This effect in our case, will be the one that has the ref flag that has already used previously. Because React detects the flag it will then attach the DOM node into the ref if it's an object of pass it as a parameter if it's a function (see callback ref in the previous part).
I want to make an article dedicated to how works React under the hood, hoping you will enjoyed it. If it's case do not hesitate to tell me in comments to give me motivation <3
Conclusion
React ref has multiple use cases that we have seen previously, do not hesitate to tell when you are using them.
The things you need to keep in mind:
- changing a
ref
will no trigger a re-render - do not update / read a
ref
directly inrender
but inuseEffect
/useLayoutEffect
and event handlers. Except when doing lazily initialization. - do not overused React state when in fact you do not need to use the value for the UI.
- when you use a
ref
to prevent putting a dependency onuseEffect
/useLayoutEffect
oruseCallback
that should not trigger the execution of the effect / re-creation of the callback. Do not forget to update in auseEffect
/useLayoutEffect
. In a next article, we will see thatref
s are also useful to use the native hook nameduseImperativeHandle
.
Do not hesitate to comment and if you want to see more, you can follow me on Twitter or go to my Website.
Top comments (4)
This was perfect article 👍👍
Oh thank you so much :*
Thers was a mistake in the article.
function MyComponent() {
const renderCount = useRef(1);
useEffect(() => {
renderCount.current++;
});
return
Number of render: {renderCount.current}
;}
Oh thanks. I fixed it :)