maesblog

Reactの「Render Props」について – 公式ドキュメント日本語訳

昨年から Reactl界隈で「Render Props」という言葉をよく目にするようになりました。Render Props は、React のコンポーネントを再利用可能な方法でその状態や振る舞いをカプセル化するためのパターンのひとつです。同様の目的でよく使われるパターンに Higher-Order Components (HOC) がありますが、Render Props は HOC の問題を解決したもので、今後主抽象化パターンの本流になりうると言われています。この Render Props を全然追いきれていなかったので、今回 React の公式ドキュメントの『Render Props』 の部分を日本語に訳してみました。

The term “render prop” refers to a simple technique for sharing code between React components using a prop whose value is a function.

“render prop” という言葉は、値が関数となる prop を使って、React のコンポーネント間でコードをシェアするためのシンプルなテクニックを指します。

A component with a render prop takes a function that returns a React element and calls it instead of implementing its own render logic.

render prop を持つコンポーネントは、React エレメントを返す関数を受け取り、独自のレンダリングロジックを実装する代わりにそれを呼び出します。

<DataProvider render={data => (
  <h1>Hello {data.target}</h1>
)}/>

Libraries that use render props include React Router and Downshift.

render props を使っているライブラリには、React RouterDownshift が含まれます。

In this document, we’ll discuss why render props are useful, and how to write your own.

このドキュメントでは、なぜ render props が役に立つのか、どのように自分で書くのかについて議論していきます。

Use Render Props for Cross-Cutting Concerns: 横断的関心事のために Render Props を使う

Components are the primary unit of code reuse in React, but it’s not always obvious how to share the state or behavior that one component encapsulates to other components that need that same state.

コンポーネントは、React においてコードを再利用する際の主要な単位です。しかし、あるコンポーネントが、カプセル化している状態や振る舞いを、同じ状態を必要とする他のコンポーネントに対してシェアする方法は常に明白ではありません。

For example, the following component tracks the mouse position in a web app:

たとえば、以下は Web アプリのマウスポジションをトラッキングするコンポーネントです。


class MouseTracker extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
        <h1>Move the mouse around!</h1>
        <p>The current mouse position is ({this.state.x}, {this.state.y})</p>
      </div>
    );
  }
}

As the cursor moves around the screen, the component displays its (x, y) coordinates in a <p>.

マウスが画面の中を動くとき、コンポーネントはその (x, y) 座標を <p> に表示します。

Now the question is: How can we reuse this behavior in another component? In other words, if another component needs to know about the cursor position, can we encapsulate that behavior so that we can easily share it with that component?

さて、ここで問題です。この振る舞いを他のコンポーネントで再利用するにはどうすればよいでしょうか?違う言い方をすると、もし他のコンポーネントがカーソルの位置について知る必要があった場合、われわれはその振る舞いをカプセル化することができ、簡単にその振る舞いをそのコンポーネントを使って共有できるでしょうか?

Since components are the basic unit of code reuse in React, let’s try refactoring the code a bit to use a <Mouse> component that encapsulates the behavior we need to reuse elsewhere.

コンポーネントは React におけるコードの再利用の基本的な単位なので、他で再利用が必要な振る舞いをカプセル化する <Mouse> コンポーネントを使うようにコードを少しリファクタリングしてみましょう。

// The <Mouse> component encapsulates the behavior we need...
// <Mouse> コンポーネントは必要な振る舞いをカプセル化します...
class Mouse extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>

        {/* ...but how do we render something other than a <p>? */}
        {/* ...しかし、<p> 以外に何かをレンダリングするにはどうしますか? */}
        <p>The current mouse position is ({this.state.x}, {this.state.y})</p>
      </div>
    );
  }
}

class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>Move the mouse around!</h1>
        <Mouse />
      </div>
    );
  }
}

Now the <Mouse> component encapsulates all behavior associated with listening for mousemove events and storing the (x, y) position of the cursor, but it’s not yet truly reusable.

これで、<Mouse> コンポーネントは mousemove イベントのリスニングや (x, y) の位置の保持に関連したすべての振る舞いをカプセル化しました。しかし、まだ真に再利用可能とは言えません。

For example, let’s say we have a <Cat> component that renders the image of a cat chasing the mouse around the screen. We might use a <Cat mouse={{ x, y }}> prop to tell the component the coordinates of the mouse so it knows where to position the image on the screen.

たとえば、画面の中のマウスを追いかけるネコの画像をレンダリングする <Cat> コンポーネントがあったとします。その際に、コンポーネントが画面上のどこに画像を配置するかを知るために、<Cat mouse={{ x, y }}> prop を使ってマウスの座標をコンポーネントに伝えるかもしれません。

As a first pass, you might try rendering the <Cat> inside <Mouse>’s render method, like this:

まず最初のパスとして、<Mouse> の render メソッドの中に <Cat> をレンダリングすることを試すかと思います。

class Cat extends React.Component {
  render() {
    const mouse = this.props.mouse;
    return (
      <img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
    );
  }
}

class MouseWithCat extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>

        {/*
          We could just swap out the <p> for a <Cat> here ... but then
          we would need to create a separate <MouseWithSomethingElse>
          component every time we need to use it, so <MouseWithCat>
          isn't really reusable yet.
        */}
        {/*
          ここで、<Cat> のために <p> をスワップアウトできました。しかし、
          <MouseWithSomethingElse> を使う必要がある時はいつでも、
          別の <MouseWithSomethingElse> コンポーネントを作成する必要が出てきます。
          したがって、<MouseWithCat> はまだ真に再利用可能であるとは言えません。
        */}
        <Cat mouse={this.state} />
      </div>
    );
  }
}

class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>Move the mouse around!</h1>
        <MouseWithCat />
      </div>
    );
  }
}

This approach will work for our specific use case, but we haven’t achieved the objective of truly encapsulating the behavior in a reusable way. Now, every time we want the mouse position for a different use case, we have to create a new component (i.e. essentially another <MouseWithCat>) that renders something specifically for that use case.

このアプローチは、特定のユースケースでは機能するでしょう。しかし、これだと再利用可能な方法で振る舞いをカプセル化するという目的を完全には達成していません。異なるユースケースでマウスポジションが必要となれば、そのユースケースのために特定のものをレンダリングする新しいコンポーネント(たとえば、本質的にもうひとつの <MouseWithCat>)を作成する必要があります。

Here’s where the render prop comes in: Instead of hard-coding a <Cat> inside a <Mouse> component, and effectively changing its rendered output, we can provide <Mouse> with a function prop that it uses to dynamically determine what to render–a render prop.

ここで render prop の登場です。<Mouse> の中に <Cat> をハードコーディングし、レンダリングされるアウトプットを実質的に変更させる代わりに、動的にレンダリングするものを決定するために使う関数の prop(render prop)を持った <Mouse> を規定することができます。

class Cat extends React.Component {
  render() {
    const mouse = this.props.mouse;
    return (
      <img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
    );
  }
}

class Mouse extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>

        {/*
          Instead of providing a static representation of what <Mouse> renders,
          use the `render` prop to dynamically determine what to render.
        */}
        {/*
          <Mouse> が何をレンダリングするかについて静的な表現を提供する代わりに、
          何をレンダリングするか動的に決定する `render` prop を使います。
        */}
        {this.props.render(this.state)}
      </div>
    );
  }
}

class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>Move the mouse around!</h1>
        <Mouse render={mouse => (
          <Cat mouse={mouse} />
        )}/>
      </div>
    );
  }
}

Now, instead of effectively cloning the <Mouse> component and hard-coding something else in its render method to solve for a specific use case, we provide a render prop that <Mouse> can use to dynamically determine what it renders.

つまり、実質的に <Mouse> コンポーネントをクローンすることや、その render メソッドの中で、特定のユースケースを解決するために、他の何かをハードコーディングすることの代わりに、<Mouse> が何をレンダリングするか動的に決定するために使うことができる render prop を規定します。

More concretely, a render prop is a function prop that a component uses to know what to render.

もっと具体的に言うと、render prop は、コンポーネントが何をレンダリングするか知るために使う関数 prop です。

This technique makes the behavior that we need to share extremely portable. To get that behavior, render a <Mouse> with a render prop that tells it what to render with the current (x, y) of the cursor.

このテクニックは、シェアする必要がある振る舞いを非常に移植性の高いものにします。この振る舞いを取得するために、カーソルの現在の (x, y) を使って何をレンダリングするか伝える render prop を持った <Mouse> をレンダリングしましょう。

One interesting thing to note about render props is that you can implement most higher-order components (HOC) using a regular component with a render prop. For example, if you would prefer to have a withMouse HOC instead of a <Mouse> component, you could easily create one using a regular <Mouse> with a render prop:

render props について注意すべきひとつの興味深いことは、render prop を持った通常のコンポーネントを使うことで、ほとんどの higher-order components (HOC) を実装できるということです。たとえば、もし <Mouse> コンポーネントの代わりに withMouse HOC を使いたかったとして、render prop を持った通常の <Mouse> を使うことで、簡単にそれを作成することができます。

// If you really want a HOC for some reason, you can easily
// create one using a regular component with a render prop!
// もし何らかの理由で HOC を使いたい場合、
// render prop を持った通常のコンポーネントでそれを作成することが簡単にできます!
function withMouse(Component) {
  return class extends React.Component {
    render() {
      return (
        <Mouse render={mouse => (
          <Component {...this.props} mouse={mouse} />
        )}/>
      );
    }
  }
}

So using a render prop makes it possible to use either pattern.

したがって、render prop を使えば、どんなパターンでも使うことが可能となります。

Using Props Other Than render: render 以外の Props の使用

It’s important to remember that just because the pattern is called “render props” you don’t have to use a prop named render to use this pattern. In fact, any prop that is a function that a component uses to know what to render is technically a “render prop”.

重要なことで、このパターンは、“render props” と呼ばれていますが、このパターンを使うために、prop 名を必ずしも render とする必要はないということを忘れないでください。実際、コンポーネントが何をレンダリングするか知るために使う関数が props であれば、技術的にはどれも “render prop” となります。

Although the examples above use render, we could just as easily use the children prop!

上記の例では render を使っていますが、同様に children prop として使うこともできます!

<Mouse children={mouse => (
  <p>The mouse position is {mouse.x}, {mouse.y}</p>
)}/>

And remember, the children prop doesn’t actually need to be named in the list of “attributes” in your JSX element. Instead, you can put it directly inside the element!

そして、children prop は、実際にJSXの要素の「属性」のリストの中で命名される必要がないということも忘れないでください。その代わりに、それを要素の内部に直接置くことができます。

<Mouse>
  {mouse => (
    <p>The mouse position is {mouse.x}, {mouse.y}</p>
  )}
</Mouse>

You’ll see this technique used in the react-motion API.

react-motion API で使われるこのテクニックを確認できます。

Since this technique is a little unusual, you’ll probably want to explicitly state that children should be a function in your propTypes when designing an API like this.

このテクニックは、そこまで一般的ではないので、以下のような API をデザインする時に、childrenpropTypes の中で関数であるべきとおそらく明示的に述べたいはずです。

Mouse.propTypes = {
  children: PropTypes.func.isRequired
};

Caveats: 注意事項

Be careful when using Render Props with React.PureComponent: React.PureComponent で Render Props を使うときは注意してください

Using a render prop can negate the advantage that comes from using React.PureComponent if you create the function inside a render method. This is because the shallow prop comparison will always return false for new props, and each render in this case will generate a new value for the render prop.

もし render メソッドの中で関数を作成した場合、render prop を使うと、React.PureComponent を使うことで得られるアドバンテージが無効となります。これは、shallow prop comparison(浅い prop の比較)が新しい props に対して常に false を返すことになり、このケースにおいて、それぞれの render は、render prop のために新しい値を生成することになるからです。

For example, continuing with our <Mouse> component from above, if Mouse were to extend React.PureComponent instead of React.Component, our example would look like this:

たとえば、上記した <Mouse> コンポーネントに対して、もし MouseReact.Component の代わりに React.PureComponent を継承したとしたら、以下のようになるでしょう。

class Mouse extends React.PureComponent {
  // Same implementation as above...
  // 上記と同じ実装...
}

class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>Move the mouse around!</h1>

        {/*
          This is bad! The value of the `render` prop will
          be different on each render.
        */}
        {/*
          これはよくないです!`render` prop の値は、
          それぞれの render で異なったものとなります。
        */}
        <Mouse render={mouse => (
          <Cat mouse={mouse} />
        )}/>
      </div>
    );
  }
}

In this example, each time <MouseTracker> renders, it generates a new function as the value of the <Mouse render> prop, thus negating the effect of <Mouse> extending React.PureComponent in the first place!

この例では、<MouseTracker> がレンダリングする度に、<Mouse render> prop の値として新しい関数が生成されます。このように React.PureComponent を継承している <Mouse> の効果は最初から無効となります。

To get around this problem, you can sometimes define the prop as an instance method, like so:

この問題を避けるために、状況に応じて、prop を以下のようにインスタンスメソッドとして定義するとよいです。

class MouseTracker extends React.Component {
  // Defined as an instance method, `this.renderTheCat` always
  // refers to *same* function when we use it in render
  renderTheCat(mouse) {
    return <Cat mouse={mouse} />;
  }

  render() {
    return (
      <div>
        <h1>Move the mouse around!</h1>
        <Mouse render={this.renderTheCat} />
      </div>
    );
  }
}

In cases where you cannot define the prop statically (e.g. because you need to close over the component’s props and/or state) <Mouse> should extend React.Component instead.

prop を静的に定義できない場合は(たとえば、コンポーネントの props かつ/または state を閉じ込める必要があるという理由などで)、<Mouse> には代わりに React.Component を継承させるようにするべきです。

訳者まとめ

以上、Render Props の翻訳となります。なぜ Render Props が必要なのかという経緯から、Render Props の使用方法、そして使用する際の注意事項まで細かく書いてくれているので、一通り読めば十分理解できるようになっているかと思います。イメージ的には関数型プログラミングでいう部分適用のような感じですね。

今回の記事は若干長かったり、翻訳だということもあったりして、ポイントがわかりにくいと思われたら、以下の記事をお勧めします。Render Props の使い方であれば、私は以下の記事を読むだけでも十分に理解することができました。

この記事内でも出てきましたが、次回は higher-order components (HOC) についての公式ドキュメントのページを訳したいと思っています。

関連記事

コメント

  • 必須

コメント