ReactでDOMにアクセス(Refs)

昨今のフロントサイドのフレームワーク・ライブラリはコンポーネントベースのアーキテクチャを取っているため、jQueryのようにDOMを直接操作する機会は少ないです。各フレームワークではDOMを参照するためのAPIが提供されていますが、用途はある程度限られています。

Reactの場合は、stateやpropsで実現できないケースでのみ利用するようドキュメントに記載されています。例えば、要素のフォーカス・テキスト選択などです。

Refs

ReactではRefsという機能を使うことでDOMへのアクセスが可能となります。また、RefsはDOMだけでなく、Reactコンポーネント要素に対してもアクセスが可能です。

Refsは簡単に言うならばDOMへの参照です。作成方法は3つあります。

  1. 文字列を使う方法(非推奨・削除予定)
  2. コールバックを使う方法
  3. React.createRefメソッドを使う方法(v16.3以降)

文字列を使う方法は今後削除予定のため、今回は2と3の方法を取り上げます。

コールバックを使う方法

Refsを作成するためのコールバック関数を参照したい要素のref属性に定義します。コールバック関数内の引数には、ReactコンポーネントのインスタンスかHTMLのDOM要素を受け取ります。どちらを受け取るかは、refを定義している要素によります。

// render()の中とする
<input type="text" ref={elem => this.myInput = elem} />

this.myInputにはDOMの参照が格納されています。これは定義しているコンポーネント内から自由に利用することができます。

const value = this.myInput.value;

例えば、上記はinput要素の値を変数に代入しています。

インライン関数の注意点

先の例のようにインラインでコールバック関数を定義する場合、コンポーネントが更新されるたび、コールバック関数が2回呼ばれます。1回目はnull、2回目にDOMが返ります。これは関数の新しいインスタンスがレンダリングごとに作成されているためです。

これが問題になる場合は、以下でこの動作を回避することができます。

class SomeComponent extends React.Component {
    constructor(props) {
        super(props);
        this.setInput = elem => {
            this.myInput = elem;
        }        
    }

    // コンポーネント内のメソッドなどで使用
    const value = this.myInput.value;

    //...略
    render() {
        return (
            <div>
				<input type="text" ref={this.setInput} />
            </div>
        );
    }
}

React.createRefを使う方法

React 16.3から追加された方法です。React.createRef()を呼び出してrefを作成します。

// 1. コンストラクタ内などでrefを作成
this.myInput = React.createRef();

// 2. 参照要素にref属性を定義する
<input type="text" ref={this.myInput} />

// 3. 利用箇所でcurrent属性を通してアクセスする
const value = this.myInput.current.value;

参照しているDOMノードは、refのcurrent属性からアクセスできます。currentまでがDOMノードの参照なので、その値を参照するにはvalueを続けます。

以下に簡単な例を示します。

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.myInput = React.createRef();
  }

  componentDidMount() {
    this.myInput.current.focus();
  }

  render() {
    return (
      <div>
        <input type="text" ref={this.myInput} />
      </div>
    );
  }
}

実行すると、input要素にフォーカスされた状態で開始されます。ユーザーはフォームにマウスを移動する必要はなく、入力を始めることができます。これはネイティブのHTMLElementObject.focus()をrefで実現しています。

Reactコンポーネントを参照する

最初に書いたようにReactのRefsはDOMだけでなく、Reactコンポーネントに対しても利用できます。Refsは過度に使用すべきものではありませんが、コンポーネントを参照するのは興味深いものでもあります。

例えば、親から子コンポーネントのメソッドを操作することもできます。以下のようなコンポーネントを考えてみましょう。

class SomeComponent extends React.Component {
  constructor(props) {
    super(props);
    this.copyText = this.copyText.bind(this);
    this.myInput = React.createRef();
  }

  copyText() {
    if (this.myInput.current.value.length > 0) {
      this.myInput.current.select();
      document.execCommand("copy");
      alert("クリップボードにコピーしました: " + this.myInput.current.value);
    }
  }

  render() {
    return (
      <span>
        <input type="text" ref={this.myInput} />
      </span>
    );
  }
}

先程同様、React.createRef()でrefを作成しています。また、参照しいてるInput要素の値を選択した後、クリップボードにコピーするcopyText()メソッドを定義しています。このコンポーネント自身にメソッドを呼び出す手段は定義されていない点に注意しておきましょう。

次に上記のコンポーネントを使用する親コンポーネントMyComponentを作成します。

const MyComponent = props => {
  const formInput = React.createRef();

  const handleClick = () => {
    formInput.current.copyText();
  };

  return (
    <div>
      <SomeComponent ref={formInput} />
      <button onClick={handleClick}>コピー</button>
    </div>
  );
};

React.createRef()を使用していますが、ref属性は先程作成したコンポーネントに定義しています。これでSomeComponentインスタンス(子)をMyComponent(親)で利用することができます。結果、ボタンをクリックしたときに先のcopyText()を呼び出すことができます。

サンプルデモ

この方法を使えば、親から子コンポーネントの値を参照して使い回すことも可能です。