React useRef の current が null で参照できない
useRef(null) の current は最初のレンダリング時には null。
DOM 参照は副作用 (useEffect) 内で読む、forwardRef 対象でないコンポーネントには ref が渡らない点をふまえて使い分ける。
#useRef#ref#null#forwardRef#useEffect
公開:
要約
useRef(null) の .current はマウント前は null。
レンダリング関数の本体で読むと当然 null。
DOM に触るのは useEffect 以降、子コンポーネントに ref を通すなら forwardRef でラップ、の 2 点を守れば大半は解消する。
よくある原因
- レンダリング中に参照している:
<div ref={ref}>を返す関数の中でref.current.focus()を呼ぶと、まだ DOM が無い段階で実行されてしまう - forwardRef なしの子コンポーネント: 関数コンポーネントは既定で
refprops を受け取らない。<Child ref={ref} />のrefは子に届かない - TypeScript で型を緩めている:
useRef<HTMLInputElement>(null)の.currentはHTMLInputElement | null型。
null チェック漏れで実行時エラー - 条件付きでマウントされる要素:
{open && <Modal ref={ref} />}のようにアンマウントされる構成ではref.currentがタイミングでnullに戻る
解決策
1. DOM アクセスは useEffect の中で
import { useEffect, useRef } from "react";
export function Autofocus() {
const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
inputRef.current?.focus(); // マウント後に呼ばれる
}, []);
return <input ref={inputRef} />;
}useRef 公式リファレンス のとおり、.current の更新は再レンダリングを引き起こさない。
レイアウト計算が要るなら useLayoutEffect を使う。
2. 子コンポーネントへの ref は forwardRef
import { forwardRef } from "react";
export const Field = forwardRef<HTMLInputElement, { label: string }>(
function Field({ label }, ref) {
return (
<label>
{label}
<input ref={ref} />
</label>
);
}
);forwardRef でラップしない関数コンポーネントに ref を渡しても null のまま。
詳細は forwardRef のドキュメント を参照。
3. 条件マウントには callback ref
const handleRef = (node: HTMLDivElement | null) => {
if (node) {
observer.observe(node); // マウント時
} else {
observer.disconnect(); // アンマウント時
}
};
return open ? <div ref={handleRef}>...</div> : null;callback ref は null での呼び出し(アンマウント)も取れるため、IntersectionObserver / ResizeObserver の登録解除に向く。
使い分けは Manipulating the DOM with Refs の章を読むとよい。
4. null 安全に書く
inputRef.current?.focus();
const width = boxRef.current?.getBoundingClientRect().width ?? 0;TypeScript の strictNullChecks 下では ?. で安全に書くのが基本。!(非 null アサーション)は必要最小限に留める。