Open5

useRefを描画に利用しようとした者の末路

IssuyIssuy

useRef は中身が変更になってもそのことを通知しないということを覚えておいてください。.current プロパティを書き換えても再レンダーは発生しません。

.current が書き換わっても再レンダーは発生しないので、描画の制御に .current を利用するのはやめましょう

https://ja.reactjs.org/docs/hooks-reference.html#useref

IssuyIssuy

例えばformをリセットする様な、refの対象をコントロールする機能を実装するとします
このようなコードなら上手く動作します

import "./styles.css";
import { useRef } from "react";

export default function App() {
  const r = useRef<HTMLFormElement>(null);
  const onControll = () => {
    console.log("onControll", r.current);
    r.current?.reset();
  };
  console.log("render", r.current);
  return (
    <div className="App">
      <form ref={r}>
        <input />
      </form>
      <button onClick={onControll}>refをコントロール</button>
    </div>
  );
}

https://codesandbox.io/s/admiring-faraday-s8gtcc?file=/src/App1.tsx

IssuyIssuy

しかし初期にformを表示できない場合はどうでしょう?
例えばあなたがNextJSなどで開発している場合、SSGの初期Loadingを考慮する必要が発生することがあるかもしれません。
ここでの過ちは .current を使って準備が整うまでボタンを使用不可にしたことです。
追加のレンダリングが走るまで、ボタンはずっと使用不可のままになってしまいます。

import "./styles.css";
import { useRef, useState } from "react";

export default function App() {
  const r = useRef<HTMLFormElement>(null);
  const [show, setShow] = useState(false);
  const [show2, setShow2] = useState(false);
  const onControll = () => {
    console.log("onControll", r.current);
    r.current?.reset();
  };
  console.log("render", r.current);
  return (
    <div className="App">
      <div>SSGなどで特定の条件下でのみ表示したいようなケース</div>
      <div>
        formの部分はSkeletonなどLoading表示しつつ、refのコントローラーはdisabledにしておきたい
      </div>
      <button
        onClick={() => {
          setShow(true);
        }}
      >
        条件を満たす
      </button>
      <button
        onClick={() => {
          setShow2(true);
        }}
      >
        再レンダリング
      </button>
      <button onClick={onControll} disabled={Boolean(!r.current)}>
        refのコントローラー
      </button>
      {show && (
        <form ref={r}>
          <input />
        </form>
      )}
      {show2 && <div>show2のフラグが有効化</div>}
    </div>
  );
}

https://codesandbox.io/s/admiring-faraday-s8gtcc?file=/src/App2.tsx:0-1016

IssuyIssuy

なので useRef を描画の制御に利用するのはやめましょう。
そして出来ればドキュメントはちゃんと目を通しておきましょう。

上に挙げたコードには codesandbox のリンクも添えてあるので実際に触って試してみてください

IssuyIssuy

[おまけ]

useRef は中身が変更になってもそのことを通知しないということを覚えておいてください。.current プロパティを書き換えても再レンダーは発生しません。DOM ノードを ref に割り当てたり割り当てを解除したりする際に何らかのコードを走らせたいという場合は、コールバック ref を代わりに使用してください。

コールバック ref を使ってみましょう。
いい感じですね!
この場合 useRef<HTMLFormElement>(null) ではなく useRef<HTMLFormElement>() にする必要があります。前者は RefObject が、後者は MutableRefObject が返ってきます。MutableRefObject は r.current = node; のような代入が可能です。

import "./styles.css";
import { useCallback, useRef, useState } from "react";

export default function App() {
  const [ready, setReady] = useState(false);
  const r = useRef<HTMLFormElement>();
  const cr = useCallback((node: HTMLFormElement) => {
    if (node !== null) {
      console.log("node not null");
      setReady(true);
      r.current = node;
    }
  }, []);
  const [show, setShow] = useState(false);
  const onControll = () => {
    console.log("onControll", r.current);
    r.current?.reset();
  };
  console.log("render", r.current);
  return (
    <div className="App">
      <div>SSGなどで特定の条件下でのみ表示したいようなケース</div>
      <div>
        formの部分はSkeletonなどLoading表示しつつ、refのコントローラーはdisabledにしておきたい
      </div>
      <button
        onClick={() => {
          setShow(true);
        }}
      >
        条件を満たす
      </button>
      <button onClick={onControll} disabled={Boolean(!ready)}>
        refのコントローラー
      </button>
      {show && (
        <form ref={cr}>
          <input />
        </form>
      )}
    </div>
  );
}

https://codesandbox.io/s/admiring-faraday-s8gtcc?file=/src/App3.tsx