🐙

ReactのRefを理解したい

2021/12/16に公開

割と雰囲気で使っていたのでちゃんと理解したいと思います。

Refとは

基本的なRefに関しての話と、useRefを使った話の二段構成になります。まずは基本的な話から。

公式ドキュメントを読んでみましょう。

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

基本的にはあまりRefに頼らないようにしましょう、ということが随所に書かれています。
コンポーネントに親子関係がある場合、基本的に子コンポーネントの詳細は隠蔽されていますが、Refを利用すると親が子の詳細を知らなくてはならない=依存関係が生まれてしまうためですね。

Refの概要としては以下のような感じです。

  • React.createRef()で生成する
  • クラスコンポーネントで使うもので、コンポーネントの構築時にインスタンスプロパティに割り当てられる
    • 関数コンポーネントに対しては、ひと手間加えないとRefを使用することはできない
  • フォワーディングを使用すると子コンポーネントのrefを公開することができる

具体的な使用例としては以下の内容が紹介されています。

  • フォーカス、テキストの選択およびメディアの再生の管理
  • アニメーションの発火
  • サードパーティの DOM ライブラリとの統合

https://ja.reactjs.org/docs/refs-and-the-dom.html#when-to-use-refs より

業務でもフォーカスに関する機能の実装の際に活用した記憶があります。公式ドキュメント内の例でもinputへのフォーカスが紹介されていますね。

さて、実際に動かしてみましょうか。

使ってみる

ここではViteを使ってプロジェクトを作成しますが、お好みの方法でReactが動作する環境を作ってください。

https://vitejs.dev/guide/

yarn create vite learn-ref --template react-ts
cd learn-ref
yarn
yarn dev

これで localhost:3000 でReactが動きます。

そしたら App.tsx を以下の内容に書き換えましょう。

App.tsx
import React from "react";
class App extends React.Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef();
  }
  render() {
    return (
      <div ref={this.myRef}>
        <button
          onClick={() => {
           console.log(this.myRef);
          }}
        >
          button
        </button>
      </div>
    );
  }
}
    
export default App;

画面に表示されるボタンをクリックすると、コンソールにRefの内容が表示されます。こんな感じになっていることでしょう。

Object { current: div }

これはドキュメントにある、

HTML 要素に対して ref 属性が使用されている場合、React.createRef() を使ってコンストラクタ内で作成された ref は、その current プロパティとして根底にある DOM 要素を受け取ります

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

こちらの挙動ということでしょう。

ついでにカスタムコンポーネントに関する挙動も見ておきましょうか。

import React from "react";
    
class App extends React.Component {
  constructor(props) {
    super(props);
    this.customRef = React.createRef();
  }
  render() {
    return (
      <div>
        <button
          onClick={() => {
            console.log(this.customRef);
          }}
        >
          button
        </button>

        <CustomComponent ref={this.customRef} />
      </div>
    );
  }
}

class CustomComponent extends React.Component {
  constructor(props) {
    super(props);
  }
  render() {
    return <p>this is custom component</p>;
  }
}

export default App;

この場合は、currentに CustomComponent が入ったオブジェクトが表示されます。

例に倣い、

ref 属性がカスタムクラスコンポーネントで使用されるとき、ref オブジェクトはコンポーネントのマウントされたインスタンスを current として受け取ります

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

こちらの挙動が確認できるはずです。

さて、DOMが取得できることがわかったので、操作を加えてみましょうか。

import React from "react";
    
class App extends React.Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef();
  }
  render() {
    return (
      <div>
        <button
          onClick={() => {
            this.myRef.current.focus();
          }}
        >
          focus
        </button>

        <button
          onClick={() => {
            this.myRef.current.blur();
          }}
        >
          blur
        </button>

        <button
          onClick={() => {
            this.myRef.current.value = "filled";
          }}
        >
          fill
        </button>

        <input type="text" ref={this.myRef} />
      </div>
    );
  }
}

export default App;

各ボタンを押すとinputを操作できるようになりました。

Refの基本はこんなところでしょうか。続けて、関数コンポーネントにおけるRefの活用について見ていきたいと思います。

useRef / useImperativeHandle

createRefはクラスコンポーネントでしか扱えません。関数コンポーネントで活用するためには useRef を使います。

先ほどの例を関数コンポーネントに書き直すとこうなります。

import React, { useRef } from "react";
    
const App = () => {
  const ref = useRef();

  return (
    <div>
      <button
        onClick={() => {
          ref.current.focus();
        }}
      >
        focus
      </button>

      <button
        onClick={() => {
          ref.current.blur();
        }}
      >
        blur
      </button>

      <button
        onClick={() => {
          ref.current.value = "filled";
        }}
      >
        fill
      </button>

      <input type="text" ref={ref} />
    </div>
  );
};

export default App;

ここで2つのref活用法を紹介しましょう。

書き換え可能な値を保持する

本質的に useRef とは、書き換え可能な値を .current プロパティ内に保持することができる「箱」のようなものです。

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

実用性があるかは微妙ですが、コンポーネントごとにID(この例ではtimestampを使います)を持たせたいというケースを考えてみましょう。

何も考えずに書くとこうです。

import React, { useState, useRef } from "react";
    
const App = () => {
  const [state, setstate] = useState("");

  return (
    <div>
      <input
        type="text"
        value={state}
        onInput={(e) => setstate(e.target.value)}
      />

      <ChildComponent />
    </div>
  );
};

const ChildComponent = () => {
  const id = new Date().getTime();

  return (
    <div>
      <p>{id}</p>
    </div>
  );
};

export default App;

親コンポーネントのstateが更新されるたびに再描画が発生し、ChildComponentのIDが変わってしまいますね。

useRefを使って改善するとこうなります。

const ChildComponent = () => {
  const id = useRef(new Date().getTime());

  return (
    <div>
      <p>{id.current}</p>
    </div>
  );
};

useRef() を使うことと自分で {current: ...} というオブジェクトを作成することとの唯一の違いとは、useRef は毎回のレンダーで同じ ref オブジェクトを返す、ということです。

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

と記載がある通り、はじめて生成したときと同じオブジェクトを返してくれるため、再描画が走ってもIDが変わることはありません。
注記で言っているのは、以下のような実装だとidに代入しているオブジェクトは描画の度に生成されるのでだめですよ、ということですね。

const ChildComponent = () => {
  const id = {
    current: new Date().getTime(),
  };

  return (
    <div>
      <p>{id.current}</p>
    </div>
  );
};

親から触る

inputに対する操作の例では同一コンポーネント内に対象があったため普通に使えていましたが、子コンポーネント内のコンポーネントを対象とする場合について見てみましょう。

この場合はforwardRefというのが必要になります。

import React, { useState, useRef, forwardRef } from "react";

const App = () => {
  const ref = useRef();
  return (
    <div>
      <button
        onClick={() => {
          console.log(ref.current.value);
        }}
      >
        fill
      </button>
      <ChildComponent ref={ref} />
    </div>
  );
};

const ChildComponent = forwardRef((props, ref) => {
  return (
    <div>
      <input type="text" ref={ref} />
    </div>
  );
});

export default App;

ChildComponentで使ってるやつですね。

また、子コンポーネントで定義されている内容を親に共有することもできます。useImperativeHandleというやつを使います。

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

Child側に、自身にフォーカスさせる関数を定義して、親から呼べるようにしてみましょう。

import React, { useRef, forwardRef, useImperativeHandle } from "react";
    
const App = () => {
  const ref = useRef();

  return (
    <div>
      <button
        onClick={() => {
          ref.current.focus();
        }}
      >
        focus
      </button>
      <ChildComponent ref={ref} />
    </div>
  );
};

const ChildComponent = forwardRef((props, ref) => {
  const inputRef = useRef();

  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
  }));

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

export default App;

こんな具合です。親コンポーネントから子コンポーネントで定義した処理を呼ぶことができました。

たいていはpropsを工夫したり設計を見直したりすることによって、「Refを使わないと実現できない!」とならないこともあり推奨されていないのだと思いますが、いざという時のための手段として覚えておくとよいですね。

Discussion