🎃

[React] ref.current を useEffect の第二引数のリストに書かない

1 min read 3

よくこんなコード書いてないですか

  const ref = useRef(null);
  
  useEffect(() => {
    if(ref.current) {
      ...
    }
  }, [ref.current]);
  
  return (
    <div ref={ref} .../>
  );

これだとマウント時にはレンダリングされない要素に ref を渡した場合に動かなかったりします

return (
  {isShown && <div ref={ref}/>}
);

何が問題か

ref.current の変更では再レンダリングが起きないので、ref.current を useEffect の第二引数に渡しても再レンダリングが起きるまで useEffect で書いた処理は走ってくれません。

また ref.current は mutable 故に想定外の変更が起こりうるので、それを useEffect でハンドリングするのは大変です。

どうするべきか

useRef を使わずに callback ref を定義します。
クラスコンポーネントで古いですがReactの公式ドキュメントでも紹介されています。

https://reactjs.org/docs/refs-and-the-dom.html#callback-refs

以下 onChange に DOM を使った処理を書いて渡せるカスタムフックの例です。

type Args<T extends HTMLElement> = {
  cleanup: (e: T) => void;
  onChange: (e: T) => void;
};

export function useCallbackRef<T extends HTMLElement>({
  cleanup,
  onChange,
}: Args<T>): React.LegacyRef<T> {
  const [curent, setCurrent] = useState<T | null>(null);
  const ref = useCallback(
    (element: T | null) => {
      if (current) {
        cleanup(current); // 前の dom があった場合受け取ったcleanup処理が走ります
      }
      if (element) {
        onChange(element);
      }
      setCurrent(element);
    },
    [current, cleanup, onChange]
  );

  return ref;
}

callback ref をDOM要素に渡すと DOM が作成、削除される時に呼び出され、その DOM を受け取って処理することができます。なので先程の useEffect の例のように DOM が渡されたときに処理が走らなかったりすることがありません。

当方マサカリ大歓迎です。良いReactライフを。