できない.dev

React useRef の current が null で参照できない

useRef(null) の current は最初のレンダリング時には null。
DOM 参照は副作用 (useEffect) 内で読む、React 18 以前は forwardRef 対象でないコンポーネントには ref が渡らない(React 19 は ref を props として受け取れる)点をふまえて使い分ける。

公開: 更新:

実行例あり(2026-10-06 に実環境で検証)

要約

useRef(null) の .current はマウント前は null。
レンダリング関数の本体で読むと当然 null。DOM に触るのは useEffect 以降、子コンポーネントに ref を通すなら React 18 以前は forwardRef でラップ(React 19 は props の ref を受け取って渡す)、の 2 点を守れば大半は解消する。

実行例

node:20 で React 19.3.0 を動かすと、レンダリング中に読んだ ref.current は null で、.focus() は Cannot read properties of null (reading 'focus') になり、useEffect の中では INPUT が入っていた。
ref を DOM 要素へ渡していない関数コンポーネントでは current が null のまま、forwardRef でラップすると INPUT に届き、callback ref はマウント時に DIV、アンマウント時に null で呼ばれた。

$ node render_vs_effect.js
during render: current = null
during render: Cannot read properties of null (reading 'focus')
in effect: current = INPUT
$ node forward_ref.js
PlainField: ref.current = null
ForwardedField: ref.current = INPUT
$ node callback_ref.js
callback ref called with: DIV
callback ref called with: null

— 2026-10-06 時点の出力

検証環境

検証日
実行環境
node:20 Debian GNU/Linux 12 (bookworm)
バージョン
  • Node.js 20.20.2
  • npm 10.8.2
  • Python 3.11.2
  • Git 2.39.5
  • React 19.3.0

この記事の「実行例」は、上記の環境で実際にコマンドを実行して得られた出力をそのまま掲載しています。 再現手順はリポジトリの検証スクリプトとして管理し、定期的に再実行して出力を更新しています。

よくある原因

  1. レンダリング中に参照している: <div ref={ref}> を返す関数の中で ref.current.focus() を呼ぶと、まだ DOM が無い段階で実行されてしまう
  2. forwardRef なしの子コンポーネント: React 18 以前の関数コンポーネントは既定で ref props を受け取らない。<Child ref={ref} /> の ref は子に届かない。
    React 19 では ref が通常の props として子に届くが、子がそれを DOM 要素に渡していなければ current は null のまま
  3. TypeScript で型を緩めている: useRef<HTMLInputElement>(null) の .current は HTMLInputElement | null 型。
    null チェック漏れで実行時エラー
  4. 条件付きでマウントされる要素: {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>
    );
  }
);

React 18 以前は、forwardRef でラップしない関数コンポーネントに ref を渡しても null のまま。
詳細は forwardRef のドキュメント(新しいタブで開く) を参照。

React 19 では forwardRef が不要になり、関数コンポーネントは ref を通常の props として受け取れる(React 19 のリリースノート(新しいタブで開く))。
受け取った ref を DOM 要素に渡せば届く。

import type { Ref } from "react";
 
export function Field({ label, ref }: { label: string; ref?: Ref<HTMLInputElement> }) {
  return (
    <label>
      {label}
      <input ref={ref} />
    </label>
  );
}

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 アサーション)は必要最小限に留める。

この記事は役立ちましたか?