React useRefにTypeScriptで正しく型を指定する方法

問題

ReactでTypeScriptと一緒にuseRefを使うと、このようなエラーに遭遇することがあります。

const inputRef = useRef<HTMLInputElement>();

inputRef.current.focus();
// 'inputRef.current' is possibly 'undefined'.

初期値をnullにしても、別のエラーが発生します。

const inputRef = useRef<HTMLInputElement>(null);

inputRef.current.focus();
// 'inputRef.current' is possibly 'null'.

毎回if (inputRef.current)でチェックしなければならないのかと思いますが、実はuseRefのオーバーロードを理解すれば、すっきり対処できます。

解決方法

useRefには2つのオーバーロードがあります。

// 1. DOM ref用(読み取り専用の.current)
// ジェネリックにnullを含め、初期値はnull
const inputRef = useRef<HTMLInputElement>(null);
// 型: RefObject<HTMLInputElement> — currentはreadonly

// 2. 値保存用(変更可能な.current)
// 初期値の型がジェネリックと一致
const countRef = useRef<number>(0);
// 型: MutableRefObject<number> — currentは変更可能

DOM refはReactが管理するのでreadonlyで、値保存用は自分で変更するのでmutableです。

実務でよく使うパターンを紹介します。

// DOM要素の参照
const inputRef = useRef<HTMLInputElement>(null);
const divRef = useRef<HTMLDivElement>(null);

// タイマーID保存
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);

// 前回の値を追跡
const prevValueRef = useRef<string>("");

// マウント状態の追跡
const isMountedRef = useRef<boolean>(false);

イベントハンドラ内では要素が確実にマウントされているので、non-nullアサーションを使っても問題ありません。

const handleSubmit = () => {
  inputRef.current!.focus();
};

ただしuseEffect内では、条件チェックをする方が安全です。

useEffect(() => {
  if (inputRef.current) {
    inputRef.current.focus();
  }
}, []);

ポイント

  • useRef<T>(null)RefObject<T>(DOM ref用、currentはreadonly)
  • useRef<T>(initialValue)MutableRefObject<T>(値保存用、currentは変更可能)
  • DOM要素のrefは常にnullで初期化し、使用時にnullチェックを行います
  • タイマー、前回の値、外部ライブラリのインスタンスなどにはMutableRefObjectを使います