TypeScript: Typing React useRef Hook
Updated on · 5 min read|As TypeScript continues to gain popularity among the JavaScript community, it has become essential to understand how to effectively use its powerful typing system in combination with the widely-adopted React library.
One of the essential hooks in React is useRef. It has many uses, e.g. it allows preserving a value between renders without triggering a re-render, which makes it useful when working with DOM elements or third-party libraries. While useRef
is straightforward to use, typing it correctly can be a challenge, especially when working with complex types.
In this post we will explore the intricacies of working with the useRef
hook and TypeScript when it comes to scenarios where we need to imperatively modify DOM elements in React components. This is often the case when managing elements' focus or integrating third-party libraries into your React applications.
We'll walk through a practical example of controlling the focus state of an input element and demonstrate how to effectively type the useRef
hook in TypeScript for this use case. Additionally, we'll look at some common pitfalls and discuss the differences between typing with a read-only property and a mutable one.
Focusing an input with useRef
Let's consider a simple use case where we want to manually set the focus on an input element when a button is clicked. The JavaScript code for this component would look like this:
jsximport React, { useRef } from "react"; export const CustomInput = () => { const inputRef = useRef(null); const onButtonClick = () => { inputRef.current.focus(); }; return ( <div> <label htmlFor={"name"}>Name</label> <input id={"name"} placeholder={"Enter your name"} ref={inputRef} /> <button type={"button"} onClick={onButtonClick}> Focus input </button> </div> ); };
jsximport React, { useRef } from "react"; export const CustomInput = () => { const inputRef = useRef(null); const onButtonClick = () => { inputRef.current.focus(); }; return ( <div> <label htmlFor={"name"}>Name</label> <input id={"name"} placeholder={"Enter your name"} ref={inputRef} /> <button type={"button"} onClick={onButtonClick}> Focus input </button> </div> ); };
TypeScript types for immutable useRef
When we click on the Focus input button, the Name input field gets focused, so far so good.
Now we'd like to use TypeScript for this component. The error we get after converting the file to TypeScript is "Object is possibly null" for the line inputRef.current.focus();
. This makes sense since we did set null
as the initial value for inputRef
.
To fix this error, we can check that the current
property of inputRef
is not null
before calling focus
on it:
tsximport React, { useRef } from "react"; export const CustomInput = () => { const inputRef = useRef(null); const onButtonClick = () => { if (inputRef.current !== null) { inputRef.current.focus(); } }; return ( <div> <label htmlFor={"name"}>Name</label> <input id={"name"} placeholder={"Enter your name"} ref={inputRef} /> <button type={"button"} onClick={onButtonClick}> Focus input </button> </div> ); };
tsximport React, { useRef } from "react"; export const CustomInput = () => { const inputRef = useRef(null); const onButtonClick = () => { if (inputRef.current !== null) { inputRef.current.focus(); } }; return ( <div> <label htmlFor={"name"}>Name</label> <input id={"name"} placeholder={"Enter your name"} ref={inputRef} /> <button type={"button"} onClick={onButtonClick}> Focus input </button> </div> ); };
This can be simplified with the optional chaining operator, ?
:
typescriptinputRef.current?.focus();
typescriptinputRef.current?.focus();
If inputRef.current
is null
or undefined
, the expression short-circuits, and the focus
method isn't called (if we would assign the result of the call to a variable, it'd be set as undefined
in this case).
This fixes the type error, however, it creates a new one - "Property 'focus' does not exist on type 'never'". This seems strange at first as we do assign the ref to the input element later. The issue is that TypeScript infers from the default value that the inputRef
can never be anything other than null
and will type it accordingly. We do know, however, that the ref will later contain an input element, so to fix this issue, we need to explicitly tell the compiler which type of element is expected via a generic type parameter when creating the ref.
typescriptconst inputRef = useRef<HTMLInputElement>(null);
typescriptconst inputRef = useRef<HTMLInputElement>(null);
This solves the "Property does not exist on type 'never'" error, and we don't get any more type complaints from TypeScript. The final code looks as follows:
tsximport React, { useRef } from "react"; export const CustomInput = () => { const inputRef = useRef<HTMLInputElement>(null); const onButtonClick = () => { inputRef.current?.focus(); }; return ( <div> <label htmlFor={"name"}>Name</label> <input id={"name"} placeholder={"Enter your name"} /> <button type={"button"} onClick={onButtonClick}> Focus input </button> </div> ); };
tsximport React, { useRef } from "react"; export const CustomInput = () => { const inputRef = useRef<HTMLInputElement>(null); const onButtonClick = () => { inputRef.current?.focus(); }; return ( <div> <label htmlFor={"name"}>Name</label> <input id={"name"} placeholder={"Enter your name"} /> <button type={"button"} onClick={onButtonClick}> Focus input </button> </div> ); };
Mutable vs immutable ref
The current typing of inputRef
works fine for the cases where it's read-only, and we do not need to reassign its value (immutable ref). However, let's consider a scenario in which we need to manually add an event listener to an input, which can be particularly useful when working with third-party libraries. The code would look something like this:
tsximport React, { useEffect, useRef } from "react"; export const CustomInput = () => { const inputRef = useRef<HTMLInputElement>(null); useEffect(() => { inputRef.current = document.getElementById("name") as HTMLInputElement; inputRef.current.addEventListener("keypress", onKeyPress); return () => { inputRef.current?.removeEventListener("keypress", onKeyPress); }; }, []); const onKeyPress = () => { /* Handle input key press */ }; return ( <div> <label htmlFor={"name"}>Name</label> <input id={"name"} placeholder={"Enter your name"} /> <button type={"button"}>Focus input</button> </div> ); };
tsximport React, { useEffect, useRef } from "react"; export const CustomInput = () => { const inputRef = useRef<HTMLInputElement>(null); useEffect(() => { inputRef.current = document.getElementById("name") as HTMLInputElement; inputRef.current.addEventListener("keypress", onKeyPress); return () => { inputRef.current?.removeEventListener("keypress", onKeyPress); }; }, []); const onKeyPress = () => { /* Handle input key press */ }; return ( <div> <label htmlFor={"name"}>Name</label> <input id={"name"} placeholder={"Enter your name"} /> <button type={"button"}>Focus input</button> </div> ); };
Note that we need to cast the result of document.getElementById
to HTMLInputElement
, as TypeScript is unable to infer the correct element type in this case and defaults to the more generic HTMLElement
. However, we are certain that the element, in this case, is an input element, so it is safe to cast it accordingly. While the code appears functional, we encounter a TypeScript error: "Cannot assign to 'current' because it is a read-only property". Upon inspecting the current
property, we find that its type is defined as React.RefObject<HTMLInputElement>.current: any
. Diving further into the type definition for React.RefObject
, it is defined as:
typescriptinterface RefObject<T> { readonly current: T | null; }
typescriptinterface RefObject<T> { readonly current: T | null; }
So how can we make it mutable? Following the type definition for useRef
, we see that it has several overloads, the most important of which are:
typescriptfunction useRef<T>(initialValue: T): MutableRefObject<T>; function useRef<T>(initialValue: T | null): RefObject<T>;
typescriptfunction useRef<T>(initialValue: T): MutableRefObject<T>; function useRef<T>(initialValue: T | null): RefObject<T>;
When specifying null
as the default parameter, but not including it in the type param, we match the second overload for the useRef
, getting a ref object with a read-only current
property. To fix it, we need to make the type param a union type of HTMLInputElement
and null
:
typescriptconst inputRef = useRef<HTMLInputElement | null>(null);
typescriptconst inputRef = useRef<HTMLInputElement | null>(null);
This will match the MutableRefObject
overload and fix the type issue. There's also a handy note in the type definition for the hook:
markdownUsage note: if you need the result of useRef to be directly mutable, include | null in the type of the generic argument.
markdownUsage note: if you need the result of useRef to be directly mutable, include | null in the type of the generic argument.
Now the "Cannot assign to 'current' because it is a read-only property" error is fixed and the final version of the code is as follows:
tsximport React, { useEffect, useRef } from "react"; export const CustomInput = () => { const inputRef = useRef<HTMLInputElement | null>(null); useEffect(() => { inputRef.current = document.getElementById("name") as HTMLInputElement; inputRef.current.addEventListener("keypress", onKeyPress); return () => { inputRef.current?.removeEventListener("keypress", onKeyPress); }; }, []); const onKeyPress = () => { /* Handle input key press */ }; return ( <div> <label htmlFor={"name"}>Name</label> <input id={"name"} placeholder={"Enter your name"} /> <button type={"button"}>Focus input</button> </div> ); };
tsximport React, { useEffect, useRef } from "react"; export const CustomInput = () => { const inputRef = useRef<HTMLInputElement | null>(null); useEffect(() => { inputRef.current = document.getElementById("name") as HTMLInputElement; inputRef.current.addEventListener("keypress", onKeyPress); return () => { inputRef.current?.removeEventListener("keypress", onKeyPress); }; }, []); const onKeyPress = () => { /* Handle input key press */ }; return ( <div> <label htmlFor={"name"}>Name</label> <input id={"name"} placeholder={"Enter your name"} /> <button type={"button"}>Focus input</button> </div> ); };
If you are curious about using TypeScript to work with the form elements in React, you may find this post helpful: TypeScript: Typing Form Events In React.
Conclusion
In this post, we've explored the intricacies of working with the useRef
hook and TypeScript when it comes to scenarios where we need to imperatively modify DOM elements in React components. We've covered a practical example of controlling the focus state of an input element and demonstrated how to effectively type the useRef
hook in TypeScript for this use case. Moreover, we've discussed some common pitfalls and the differences between typing with a read-only property and a mutable one.
Hopefully now, by understanding the nuances of TypeScript typings and the useRef
hook, you can approach more complex scenarios in your React applications with confidence. Remember that when using TypeScript, it's essential to provide accurate type information to fully leverage its powerful type-checking capabilities and improve the quality and maintainability of your code.