Mutable and immutable useRef semantics with React & TypeScript
Wojciech Matuszewski
Posted on June 12, 2021
In this post, you will learn how different ways declaring a ref with useRef
hook influence the immutability of the current
ref property. We will be looking at how to make the current
property immutable, mutable, and know without much effort if the ref is one or the other.
All the behavior I'm going to talk about is only relevant in the context of TypeScript. The mutability / immutability is enforced at type level, not runtime level.
Immutable current
property
The immutable semantics of the useRef
hooks are usually used with DOM elements. A common use-case might be to get the ref of an element and focus that element whenever a button is clicked.
Here is how I would write that.
import * as React from "react";
const Component = () => {
const inputRef = React.useRef<HTMLInputElement>(null);
return (
<div>
<input type="text" name="name" ref={inputRef} />
<button type="button" onClick={() => inputRef.current?.focus()}>
Click to focus the input
</button>
</div>
);
};
Notice the type and the value I’ve initialized the useRef
with. The semantics I’ve used signal that I’m relying on React to manage the ref for me. In our case, this means that I cannot mutate the inputRef.current
. If I ever tried to do that, TypeScript would complain.
import * as React from "react";
const Component = () => {
const inputRef = React.useRef<HTMLInputElement>(null);
return (
<div>
{/* Cannot assign to 'current' because it is a read-only property */}
<input type = "text" ref = {callbackRefValue => inputRef.current = callbackRefValue}>
<button type="button" onClick={() => inputRef.current?.focus()}>
Click to focus the input
</button>
</div>
);
};
After writing similar code for a while, I’ve created a rule of thumb I follow to understand if the ref that I’m looking is immutable.
If the
useRef
is initialized withnull
and the initial value does not belong to the provided type, thecurrent
property is immutable.
In our case, the null
initial value does not belong to the type HTMLInputElement
so the current
property cannot be mutated.
Mutable current
property
To have the current
property of the ref be mutable, we need to change how we are declaring ref itself.
Suppose we are writing a component that deals with timers. The useRef
hook is an ideal candidate to hold a reference to a timer. With the timer reference at hand, we can make sure that we clear the timer when the component unmounts.
Here is an, albeit a bit contrived, example.
import * as React from "react";
const Component = () => {
const timerRef = React.useRef<number | null>(null);
// This is also a valid declaration
// const timerRef = React.useRef<number>()
React.useEffect(() => {
// Mutation of the `current` property
timerRef.current = setTimeout(/* ... */)
return clearInterval(timerRef.current)
}, [])
return (
// ...
);
};
Since in the beginning, I have no way to know what the reference to the later declared setTimeout
might be, I've initialized the useRef
with null
. Apart from the types, the declaration of the ref might seem eerily similar to the one in the Immutable current
property section.
However, since the initially provided value (in our case null
) wholly belongs to the type I've declared the useRef
with (number | null
), the current
property is allowed to be mutable.
Similarly to the immutable current
property case, here is my rule of thumb.
If the
useRef
is initialized with a value that belongs to the provided type, thecurrent
property of the ref is mutable.
In our case, the null
initial value belongs to the type number | null
so the current
property can be mutated.
As an alternative, I could have declared the timerRef
variable the following way
const timerRef = React.useRef<number>(); // the `timerRef.current` is also mutable
Why is the current
allowed to be mutated in this case? Because the timerRef
is implicitly initialized with the undefined
value. The undefined
value belongs to the type I've declared the timerRef
- the React.useRef
typings are overloaded depending on the type of the initial value.
const timerRef = React.useRef<number>();
// Really is
const timerRef = React.useRef<number>(undefined);
// The `React.useRef` type definitions specify an overload whenever the type of the initial value is `undefined`
function useRef<T = undefined>(): MutableRefObject<T | undefined>; // Notice the `MutableRefObject`.
Summary
When I started working with React & TypeScript, I found the difference between mutable and immutable refs quite confusing. I hope that this article was helpful and cleared some of the questions you might have had on the subject matter.
You can find me on twitter - @wm_matuszewski.
Thank you for your time.
Posted on June 12, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.