TypeScript: Typing React useRef hook
Tags: javascript, typescript, react, hooks
Published: 2022-08-14 17:16:21

There are cases when we need to imperatively modify DOM elements in React components outside the usual component flow.
The most common examples are managing elements' focus or using third-party libraries (especially the ones not written in React) inside React applications.

This post will demonstrate how to type `useRef` hook in TypeScript on example of controlling the focus state of an input element.

Let's say we have a simple use case where we want to manually focus input on a button click. The JS code for the component would look like this:

```javascript
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>
    );
};
```

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. As the first step we can simply change the file's extension from `.js` to `.tsx`. The error we get after converting the file to TS 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:

```typescript
if (inputRef.current !== null) {
    inputRef.current.focus();
}
```

This can be simplified with the [optional chaining operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining), `?`:

```typescript
inputRef.current?.focus();
```

If `inputRef.current` is nullish (`null` or `undefined`), the expression short-circuits and `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 weird at first as we do assign the ref to the input element later. The issue is that TS 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 the element is expected:

```typescript
const inputRef = useRef<HTMLInputElement>(null);
```

This solves the issue, and we don't get any type errors. The final code looks as follows:

```tsx
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"} ref={inputRef}/>
            <button type={"button"} onClick={onButtonClick}>
                Focus input
            </button>
        </div>
    );
};
```

### `useRef<HTMLInputElement>(null)` vs `useRef<HTMLInputElement | null>(null)`

The current typing of `inputRef` works fine for the cases where we do not need to reassign its value. Let's consider a situation where we want to manually add an event listener to an input (useful when working with 3rd party libraries). The code would look something like this:

```tsx
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`, since TS cannot infer correct element type in this case and defaults to a more generic `HTMLElement`. We do know however that the element in this case is an input element, so it's safe to cast it accordingly. While the code looks fine, we get a TS error - `Cannot assign to 'current' because it is a read-only property.` Upon inspecting the `current` property we see that its type is defined as `React.RefObject<HTMLInputElement>.current:any`. Going deeper into the type definition for `React.RefObject`, it is defined as:

```typescript
interface RefObject<T> {
    readonly current: T | null;
}
```

So how can we make it mutable? Following the type definition for `useRef`, we see that it in fact has a few overloads, the most important of which are:

```typescript
function 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 readonly `current` property. To fix it, we need to include `null` in the type param:
```typescript
const 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:

```markdown
Usage note: if you need the result of useRef to be directly mutable, include | null in the type of the generic argument.
```

The final version of the code is as follows:

```tsx
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>
    );
};
```