かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

React TypeScript: useRef の current に代入しようとしたら型エラーになった

React (TypeScript) で debounce な処理を作っていて timer を useRef で作った ref オブジェクトに格納しようとしたらエラーになった

環境

  • React 18.2.0
  • TypeScript 4.9.5

ref.current への代入で型エラー Cannot assign to 'current' because it is a read-only property.

import { useRef, useEffect } from 'react';

function MyComponent() {
  const timerRef = useRef<ReturnType<typeof setTimeout>>(null);
  // … 略
  useEffect(() => {
    timerRef.current = setTimeout(callback, timeout);
    // => Type Error
   return () => clearTimeout(timerRef.current);
  }, []);
}

timerRef.current = の代入の箇所で下記のような型エラーになった
Cannot assign to 'current' because it is a read-only property.ts(2540). (property) RefObject<NodeJS.Timeout>.current: any

null 型が含まれることを明記しせず null で初期化した ref オブジェクトは読み取り専用になる

In TypeScript, useRef returns a reference that is either read-only or mutable, depends on whether your type argument fully covers the initial value or not.
To access a DOM element: provide only the element type as argument, and use null as initial value. In this case, the returned reference will have a read-only .current that is managed by React.
cf. GitHub - typescript-cheatsheets/react: Cheatsheets for experienced React developers getting started with TypeScript

The issue is that when we pass null as an initial value to the useRef hook and don't include null in the type we passed to the hook's generic, we create an immutable ref object.
cf. (useRef) Cannot assign to 'current' because it is read-only property | bobbyhadz

解決方法: useRef の型に null を含める

ジェネリックでの型指定を null を含めた Union 型にすると ref.current に代入可能になる
上記の setTimeout のタイマーに利用したい例は下記のように変更すればOK

import { useRef, useEffect } from 'react';

function MyComponent() {
- const timerRef = useRef<ReturnType<typeof setTimeout>>(null);
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  // … 略
  useEffect(() => {
    timerRef.current = setTimeout(callback, timeout);
    // => OK
   return () => clearTimeout(timerRef.current);
  }, []);
}

Note: HTMLElement への ref

The same would be the case if your ref points to a DOM element.
You'd have to type the hook as const ref = useRef<HTMLElement | null>(null) if you need to change the value of the ref's current property.
Note that you wouldn't have to include null in the ref's type if you aren't directly assigning to its current property.

import { useRef } from 'react';

const App = () => {
  const ref = useRef<HTMLInputElement>(null);
  return <input ref={ref} type="text" defaultValue="" />;
};

The ref in the example is used to focus the input element.
There is no assignment to the ref's .current property, so it's not necessary to include null in its type.
cf. (useRef) Cannot assign to 'current' because it is read-only property | bobbyhadz

input タグなど ref= で ref オブジェクトを指定する使い方の場合 .current への代入ではないので型定義に null を含めなくても問題がないとのこと

.current に代入する ref オブジェクトは useRef<some type | null>(null) と型定義に null を含めて初期化する必要がある!
おわり


[参考]