# React - Ref

# Refs and the DOM

# 「宣言的」と「命令的」の違い

  • 宣言的 (declarative)
    • プロパティで制御する isOpenなど
    • Controlled Component という
  • 命令的 (imperative)
    • メソッドで制御する open()など
    • 子側で勝手に操作される
    • Uncontrolled Component という

宣言的に子コンポーネントを操作できる場合は、必ずそうすること。 宣言的には操作できない場合や、フォームのネイティブの動作を尊重したい場合など、ごく限られた場面においては命令的に子コンポーネントを操作する。この場合に使うのが、Ref である。

# いつ Ref を使うべきか

  • フォーカスを扱う時、テキストを選択するとき、メディアを再生するとき
  • アニメーションを発動するとき
  • サードパーティの DOM ライブラリを React で使う時

# Ref の作成

React.createRef()で作成し、要素のref属性に設定する。ref属性はclassNameなどと同じように、他のカスタムな属性とは異なる特別な役割を持っている。

コンポーネント全体で利用できるように、インスタンスプロパティに代入しておくのが一般的。

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef();
  }
  render() {
    return <div ref={this.myRef} />;
  }
}

# Ref へのアクセス

ref を 要素に設定すると、this.myRef.currentのような形で要素にアクセスできるようになる。この値は、ref を設定した要素の種類によって異なってくる。

  • HTML 要素の場合(divなど) --- HTML 要素自体
  • クラスコンポーネントの場合(MyComponentなど) --- クラスコンポーネントのインスタンス
  • ファンクションコンポーネントの場合 --- 使用不可、ただしHook を使用すれば可能 (opens new window)

# タイミング

コンポーネントがマウントされると React はcurrentに設定された変数に値を割り当て、マウント解除されるとそれを null に戻す。

currentの値の更新は、componentDidMount または componentDidUpdate ライフサイクルメソッドのに行われる。

# Callback Refs

Ref を設定するもう一つの方法。より細かい制御が可能。

ref属性に、createRef()で作った値ではなく、関数を渡す。

この関数は、コンポーネントがマウント・アンマウントされたときに呼ばれる。マウントされたときは DOM 要素またはインスタンスとともに呼ばれる。アンマウントされたときはnullとともに呼ばれる。

Ref は、componentDidMount や componentDidUpdate が呼ばれる前に実行されるので、常に最新であることが保証される。

class CustomTextInput extends React.Component {
  constructor(props) {
    super(props);

    this.textInput = null;
  }

  setTextInputRef = element => {
    this.textInput = element;
  };

  focusTextInput = () => {
    if (this.textInput) this.textInput.focus();
  };

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

# 子コンポーネントの Ref を取得する

例えば下記の例では、inputRefという属性に設定している Callback Ref により、子の Ref を取得できる。

なお、inputRefのようなカスタムな属性ではなく、ref属性で子コンポーネントの特定の要素(インスタンスではない)を取得したい場合は、後述の Ref Forwarding を使用すること。

function CustomTextInput(props) {
  return (
    <div>
      <input ref={props.inputRef} />
    </div>
  );
}

class Parent extends React.Component {
  render() {
    return <CustomTextInput inputRef={el => (this.inputElement = el)} />;
  }
}

# Ref Forwarding

# Ref を DOM に転送する

あるコンポーネントが、受け取ったrefをコンポーネント内の別の DOM 要素に割り当てる(転送する)場合に使う。React.forwardRef()を使うことで実現する。ごく限られた場面で使用し、乱用しないこと。

下記の例では、FancyButtonは、さもbutton要素であるかのように振る舞うことができる。

const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref}>{props.children}</button>
));

// You can now get a ref directly to the DOM button:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;

なお、クラスコンポーネントでは下記のようにする。

class FancyButton extends Component {
  render() {
    return <button ref={this.props.innerRef}>{this.props.children}</button>;
  }
}
export default React.forwardRef((props, ref) => (
  // カスタム属性で渡している点に注意する。
  // もし`ref`属性に設定すると、ref=FancyButtonクラスのインスタンスになってしまう。
  <FancyButton innerRef={ref} {...props} />
));

# ライブラリ作成者に向けての注意

  • forwardRef を使い始めるときは、Breaking Change として扱うべし
  • forwardRef の適用を Conditional に決定することは避けるべし

# HOC と forwardRef を組み合わせて使う方法

HOC で forwardRef を使いたい場合は、ref属性を手動で設定する必要がある。

function logProps(Component) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps);
      console.log('new props:', this.props);
    }

    render() {
      const { forwardedRef, ...rest } = this.props;

      // Assign the custom prop "forwardedRef" as a ref
      return <Component ref={forwardedRef} {...rest} />;
    }
  }

  return React.forwardRef((props, ref) => {
    // forwardedRefというカスタム属性を使っている点に注目。
    // ここでref属性として渡してしまうと、ref=LogPropsのインスタンスになってしまう。
    return <LogProps {...props} forwardedRef={ref} />;
  });
}