🥢

React useRefのパターン ~どのようなときに使う?~

2022/02/23に公開

useRefとは

公式サイトによれば、

useRef は、.current プロパティが渡された引数 (initialValue) に初期化されているミュータブルな ref オブジェクトを返します。返されるオブジェクトはコンポーネントの存在期間全体にわたって存在し続けます。

まずは ref のことを DOM にアクセスする手段として理解しているかもしれません。<div ref={myRef} /> のようにして React に ref オブジェクトを渡した場合、React は DOM ノードに変更があるたびに .current プロパティをその DOM ノードに設定します。

しかしながら useRef() は ref 属性で使うだけではなく、より便利に使えます。これはクラスでインスタンス変数を使うのと同様にして、あらゆる書き換え可能な値を保持しておくのに便利です。

と書いています。useRefの使いみちに2つあることがわかります。

  1. 生domを保持し、それを使い回す。生domを入れる変数のような使い方。
  2. レンダリングを起こさず値を保持する使い方(useStatesetStateするたびにレンダリングが走るので、レンダリングさせたくないときはuseRefを使えばよい)。

1つ目は生domが提供するAPIを直接使いたいときに使います。例えばフォームのフォーカスはReactだけの機能では実現できないので、フォームのdomを取得して、focusを実行します。

賛否両論あるとは思いますが、2つ目の使い方はuseStateとメモ化である程度回避できると思います。React wayに沿わない(宣言的よりも命令的なコードになりがち)というイメージから僕はあまり積極的にuseStateよりもuseRefをこの方法で使おうとは思いません。ネットには2つ目の使い方の記事がたくさんあるのでそちらを参照してみてください。

今回は1つ目の使い方のサンプルコードをいくつか例示してみたいと思います。

Reactだけでは実現できない生domの機能を使う

以下が代表的な例でしょうか。

  • フォームにフォーカス
  • ある要素までスクロールする

フォームにフォーカス

これはinputタグが提供するfocusという機能を使います。
https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/focus

以下はコンポーネントがマウントされたときに、テキストエリアにフォーカスするサンプルです。

const App: React.VFC = () => {
  const ref = useRef<HTMLInputElement>(null);

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

  return (
    <div>
      <input ref={ref} type="textarea" />
    </div>
  );
};

ある要素までスクロール

ある要素までスクロールするにはscrollIntoViewという機能を使います。
https://developer.mozilla.org/ja/docs/Web/API/Element/scrollIntoView

const List = () => {
  const ref = useRef<HTMLDivEelement>(null);
  
  const scroll = () => {
    // 以下はtypescriptの書き方。jsの場合は
    // if(scrollBottomRef && scrollBottomRef.current) {
    //   scrollBottomRef.current.scrollIntoView()
    // }
    ref?.current?.scrollIntoView();
  }
  
  const items: string[] = [];
  return (
    <div>
      <div>aaa</div>
      <div>aaa</div>
      <div ref={ref}>ここにスクロール</div>
      <div>aaa</div>
      <div>aaa</div>
      <button type="button" onClick={scroll}>スクロール</button>
    </div>
  )
}

よければこちらも参考にしてください。
https://zenn.dev/dove/articles/be3fff0e84729d

標準で提供されているカスタマイズ性の乏しいフォームを隠して、独自デザインのものを提供する

以下が代表的な例でしょうか。

  • ファイルアップロードボタン
  • チェックボックス

ファイルアップロードボタン

  1. まずはinputタグを隠します。
  2. inputタグの生domを保持します。
  3. Buttonがクリックされたら、保持していたinputタグの生domをクリックします。
  4. するとinputタグのコールバックに選択したファイルが渡されます。
import React, { useRef } from "react";
import { render } from "react-dom";

const App: React.FC = () => {
  const inputRef = useRef(null);

  const onFileInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    console.log(event.target.files[0]);
  };

  const fileUpload = () => {
    console.log(inputRef.current);
    inputRef.current.click();
  };

  return (
    <div>
      <button onClick={fileUpload}>ファイルアップロード</button>
      <input
        hidden
        ref={inputRef}
        type="file"
        accept="image/*"
        onChange={onFileInputChange}
      />
    </div>
  );
};

render(<App />, document.getElementById("root"));

こちらに詳しくかいたのでよければ見てください。
https://zenn.dev/dove/articles/1927889e1c4153

チェックボックス

標準のチェックボックスを隠し、spanタグなどに、チェックボックスの見た目をもたせます。こちらの記事が詳しいですね。

https://dev.to/tomdohnal/custom-checkbox-in-react-animated-and-accessible-3jk9

記事中のチェックボックスの隠し方について補足です。アクセシビリティの観点からチェックボックスを完全に隠すのではなく、縦横1pxにして存在はするけど、見えないようにしています。こうすることでスクリーンリーダーがチェックボックスを扱えるようになります。

/* taken from https://css-tricks.com/inclusively-hidden/ */
input[type="checkbox"] {
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  height: 1px;
  overflow: hidden;
  position: absolute;
  white-space: nowrap;
  width: 1px;
}

レンダリングされたあとのwidthやheightなどを知って、それらを計算して新しい機能を提供する

Reactが流行る以前のjsのライブラリといえば、生domの実際の値を計測して、再調整することが多かったですね。React上でこれらを実現するには、やはりrefを使います。

例えば以前投稿したMinimapでは

  • ミニマップにする前のdomの子domの高さを取得
  • 画面スクロール
    を実現するために、複数のrefをMinimapコンポーネントに渡しています。

https://zenn.dev/dove/articles/ed2b15c40a9669

Discussion