最近、Higher-Order Component (HOC) や Render Props といった言葉を最近よく目にするようになりました。これらは、React のコンポーネントを再利用可能な方法でその状態や振る舞いを抽象化するためのパターンです。この辺の書き方を完全には追いきれていなかったので、React の公式ドキュメントを訳すことにしました。前回は Render Props の部分を日本語に訳しました。今回は、Higher-Order Component の部分を訳してみます。
A higher-order component (HOC) is an advanced technique in React for reusing component logic. HOCs are not part of the React API, per se. They are a pattern that emerges from React’s compositional nature.
higher-order component (HOC) は、コンポーネントのロジックを再利用するための React の上級者向けテクニックです。HOCs は、それ自体は React API の一部ではありません。React の構造的な性質から出てきたパターンです。
Concretely, a higher-order component is a function that takes a component and returns a new component.
具体的には、Higher-Order component は、コンポーネントを受け取り、新しいコンポーネントとして返す関数です。
Whereas a component transforms props into UI, a higher-order component transforms a component into another component.
コンポーネントは props を UI に変換するのに対して、Higher-Order component は、コンポーネントを別のコンポーネントに変換します。
HOCs are common in third-party React libraries, such as Redux’s connect and Relay’s createFragmentContainer.
HOCs は、Redux の connect や Relay の createFragmentContainer のように、サードパーティの React ライブラリでは一般的です。
In this document, we’ll discuss why higher-order components are useful, and how to write your own.
このドキュメントでは、なぜ Higher-Order components が役に立つのか、どのように自分で書くのかについて議論していきます。
Use HOCs For Cross-Cutting Concerns – 横断的関心事のために HOCs を使う
Note
We previously recommended mixins as a way to handle cross-cutting concerns. We’ve since realized that mixins create more trouble than they are worth. Read more about why we’ve moved away from mixins and how you can transition your existing components.
注意
わたしたちは、以前は横断的関心事をハンドリングするための方法として mixins を推奨していました。その後、mixins はそれらがもたらす価値以上に問題を引き起こすと理解するようになりました。なぜわたしたちが mixins から遠ざかったか、そしてどうすれば mixins を使った既存のコンポーネントを移行できるかについての詳細はこちらをお読みください。
Components are the primary unit of code reuse in React. However, you’ll find that some patterns aren’t a straightforward fit for traditional components.
コンポーネントは、React におけるコードを再利用する際の主要な単位です。しかしながら、いくつかのパターンは従来のコンポーネントにはあまり適していないことがわかるでしょう。
For example, say you have a CommentList component that subscribes to an external data source to render a list of comments:
たとえば、以下はコメントのリストをレンダリングするために外部データソースを subscribe(購読)する CommentList
コンポーネントです。
Later, you write a component for subscribing to a single blog post, which follows a similar pattern:
次は、似たようなパターンに従う、ひとつのブログ投稿を subscribe するコンポーネントを書きます。
CommentList and BlogPost aren’t identical — they call different methods on DataSource, and they render different output. But much of their implementation is the same:
CommentList
と BlogPost
は同じものではありません。これらは、DataSource
上の異なるメソッドを呼び、異なったアウトプットをレンダリングします。しかし、これらの実装はほとんど同じです。
・On mount, add a change listener to DataSource.
・Inside the listener, call setState whenever the data source changes.
・On unmount, remove the change listener.
- マウント時に、変更リスナーを
DataSource
に追加します - リスナーの内部で、データのソースが変更されるたびに
setState
を呼び出します - アンマウント時に、変更リスナーを削除します
You can imagine that in a large app, this same pattern of subscribing to DataSource and calling setState will occur over and over again. We want an abstraction that allows us to define this logic in a single place and share it across many components. This is where higher-order components excel.
巨大なアプリでは、この DataSource
を subscribe し、setState
を呼び出す同じパターンが何度も繰り返し行われるということが想像できるでしょう。このロジックをひとつの場所に定義し、多くのコンポーネント間で共有できるようにする抽象化が必要です。Higher-Order components が優れている部分がまさにここです。
We can write a function that creates components, like CommentList and BlogPost, that subscribe to DataSource. The function will accept as one of its arguments a child component that receives the subscribed data as a prop. Let’s call the function withSubscription:
DataSource
を subscribe する CommentList
や BlogPost
のようなコンポーネントを作る関数を書くことができます。その関数は、prop を通して subscribe されたデータを受け取る子コンポーネントを、引数を通して受け取ることになります。withSubscription
関数を呼び出してみましょう。
The first parameter is the wrapped component. The second parameter retrieves the data we’re interested in, given a DataSource and the current props.
第一引数は、ラップされるコンポーネントです。第二引数は、DataSource
と 現在の props が渡され、使用したいデータを取得します。
When CommentListWithSubscription and BlogPostWithSubscription are rendered, CommentList and BlogPost will be passed a data prop with the most current data retrieved from DataSource:
CommentListWithSubscription
と BlogPostWithSubscription
がレンダリングされると、DataSource
から取得される最新のデータと共に data
prop が CommentList
と BlogPost
に渡されます。
Note that a HOC doesn’t modify the input component, nor does it use inheritance to copy its behavior. Rather, a HOC composes the original component by wrapping it in a container component. A HOC is a pure function with zero side-effects.
HOC は input されたコンポーネントを修正せず、その振る舞いをコピーするために継承を使うということに注意してください。むしろ、HOC はコンテナーコンポーネントの中にコンポーネントをラップすることで、オリジナルのコンポーネントを生成します。HOC は副作用のない純粋関数です。
And that’s it! The wrapped component receives all the props of the container, along with a new prop, data, which it uses to render its output. The HOC isn’t concerned with how or why the data is used, and the wrapped component isn’t concerned with where the data came from.
そしてこれです!ラップされたコンポーネントは、そのアウトプットをレンダリングするために使う data
である、新しい prop に加えて、コンテナーのすべての props を受け取ります。HOC は、データがどのように、またはなぜ使われるかについて関心を持たず、ラップされたコンポーネントは、そのデータがどこから来るのかについて関心を持ちません。
Because withSubscription is a normal function, you can add as many or as few arguments as you like. For example, you may want to make the name of the data prop configurable, to further isolate the HOC from the wrapped component. Or you could accept an argument that configures shouldComponentUpdate, or one that configures the data source. These are all possible because the HOC has full control over how the component is defined.
withSubscription
は、普通の関数なので、多くも少なくも好きなように引数を追加することができます。たとえば、ラップされたコンポーネントから HOC をはるか遠くに隔離するために、data
prop の名前を設定変更可能にしたいかもしれません。あるいは、shouldComponentUpdate
を設定する引数、またはデータソースを設定する引数を取得できたらよいと思うかもしれません。これらはすべて可能です。なぜなら HOC はコンポーネントがどのように定義されているか完全に制御しているからです。
Like components, the contract between withSubscription and the wrapped component is entirely props-based. This makes it easy to swap one HOC for a different one, as long as they provide the same props to the wrapped component. This may be useful if you change data-fetching libraries, for example.
コンポーネントのように、withSubscription
と ラップされたコンポーネント間の結びつきは完全に props ベースです。このことにより、同じ props をラップされたコンポーネントに提供している限り、ある HOC を異なる HOC と交換することが簡単になります。たとえば、データをフェッチするライブラリを変更したい場合、このことが役に立つかもしれません。
Don’t Mutate the Original Component. Use Composition. – オリジナルのコンポーネントを mutate しない。Composition を使う。
Resist the temptation to modify a component’s prototype (or otherwise mutate it) inside a HOC.
HOC の中でコンポーネントのプロパティを修正したい(または別の方法では mutate したい)という誘惑に耐えましょう。
There are a few problems with this. One is that the input component cannot be reused separately from the enhanced component. More crucially, if you apply another HOC to EnhancedComponent that also mutates componentWillReceiveProps, the first HOC’s functionality will be overridden! This HOC also won’t work with functional components, which do not have lifecycle methods.
ここにはいくつかの問題があります。ひとつは、input されたコンポーネントは enhance(強化)されたコンポーネントから独立して再利用できないということです。より重要なことですが、もし別の HOC を EnhancedComponent
(このコンポーネントはまた componentWillReceiveProps
を mutate します)に適用する場合、最初の HOC の機能は上書きされてしまいます。この HOC はまたライフサイクルメソッドを持たない Functional Component を使うと動作しません。
Mutating HOCs are a leaky abstraction—the consumer must know how they are implemented in order to avoid conflicts with other HOCs.
HOC を Mutate することは、抽象化を破綻(Leaky Abstruction)させます。つまり、consumer(実行側)は、他の HOC とのコンフリクトを避けるためにどのように HOC を実装するか知る必要があります。
Instead of mutation, HOCs should use composition, by wrapping the input component in a container component:
mutation の代わりに、HOC は composition を使うべきです(Container コンポーネント内で input したコンポーネントをラップする方法で)。
This HOC has the same functionality as the mutating version while avoiding the potential for clashes. It works equally well with class and functional components. And because it’s a pure function, it’s composable with other HOCs, or even with itself.
この HOC は、mutate のバージョンと同じ機能を持つのと同時に、クラッシュの可能性を避けます。また、クラスおよび Functional Component でも同様に機能します。さらに、純粋関数なので、他の HOC でも、自分自身でさえも、組み立て可能です。
You may have noticed similarities between HOCs and a pattern called container components. Container components are part of a strategy of separating responsibility between high-level and low-level concerns. Containers manage things like subscriptions and state, and pass props to components that handle things like rendering UI. HOCs use containers as part of their implementation. You can think of HOCs as parameterized container component definitions.
すでに、HOC と Container Component と呼ばれるパターンの間に類似点があることに気づいているかもしれません。Container Component は、高レベルと低レベルの関心ごと間の責務を分離する戦略の一部です。Container は subscription や state のようなモノを管理し、props を UI をレンダリングするようなモノをハンドリングするコンポーネントに渡します。HOC はその実装の一部として Container を使用します。パラメータ化された Container Component 定義 として HOC を考えることができます。
Convention: Pass Unrelated Props Through to the Wrapped Component – 作法: ラップされたコンポーネントに、関係のない props を通過させる
HOCs add features to a component. They shouldn’t drastically alter its contract. It’s expected that the component returned from a HOC has a similar interface to the wrapped component.
HOC はコンポーネントに機能を追加します。そして、その結びつきを大きく変えるべきではありません。ひとつの HOC から返されるコンポーネントは、ラップされたコンポーネントに対して同じようなインターフェースを持つということが期待されます。
HOCs should pass through props that are unrelated to its specific concern. Most HOCs contain a render method that looks something like this:
HOC はその HOC に特化した関心ごととは関係のない props でも通過させるべきです。ほとんどの HOC は 以下のような render メソッドを持っています。
This convention helps ensure that HOCs are as flexible and reusable as possible.
この慣習は、HOC はできる限り柔軟で再利用可能であることを保証するのに役立っています。
Convention: Maximizing Composability – 作法: Composability を最大化する
Not all HOCs look the same. Sometimes they accept only a single argument, the wrapped component:
すべての HOC が同じようには見えるわけではありません。時には、ひとつの引数(以下のようにラップするコンポーネント)のみを受け取ることもあります。
Usually, HOCs accept additional arguments. In this example from Relay, a config object is used to specify a component’s data dependencies:
通常、HOC はさらなる引数を受け取ります。この Relay の例では、コンポーネントのデータの依存関係を指定するために config オブジェクトが使われています。
The most common signature for HOCs looks like this:
もっとも一般的な HOC のシグネチャは以下のようになります。
What?! If you break it apart, it’s easier to see what’s going on.
難しいでしょうか?その場合は分割してみると、何が起きているのか理解しやすくなります。
In other words, connect is a higher-order function that returns a higher-order component!
別の言い方をすると、connect
は Higher-Order component を返す Higher-Order function(高階関数)です。
This form may seem confusing or unnecessary, but it has a useful property. Single-argument HOCs like the one returned by the connect function have the signature Component => Component. Functions whose output type is the same as its input type are really easy to compose together.
この書き方は、複雑または不要に見えるかもしれませんが、役に立つプロパティを持っています。connect
関数によって返される HOC のようなひとつの引数を受け取る単一引数 HOC は、Component => Component
というシグネチャを持っています。アウトプットの型がインプットの型と同じである関数は、一緒に作成することが本当に簡単です。
(This same property also allows connect and other enhancer-style HOCs to be used as decorators, an experimental JavaScript proposal.)
(この同じプロパティによって、connect
や他のエンハンサースタイルの HOC を、実験的な JavaScript プロポーザルであるデコレータとして使えるようになります。)
The compose utility function is provided by many third-party libraries including lodash (as lodash.flowRight), Redux, and Ramda.
compose
ユーティリティ(効用)関数は、lodash(lodash.flowRight として)や Redux、Ramda を含む多くのサードパーティのライブラリによって提供されています。
Convention: Wrap the Display Name for Easy Debugging – 作法: 簡単にデバッグするために表示名をラップする
The container components created by HOCs show up in the React Developer Tools like any other component. To ease debugging, choose a display name that communicates that it’s the result of a HOC.
HOC によって作られる Container Component は 他のコンポーネントと同じように React Developer Tools で確認できます。デバッグを簡単にするには、HOC の結果であるということを伝えている表示名を選択してください。
The most common technique is to wrap the display name of the wrapped component. So if your higher-order component is named withSubscription, and the wrapped component’s display name is CommentList, use the display name WithSubscription(CommentList):
もっとも一般的なテクニックは、ラップされたコンポーネントの表示名をラップすることです。したがって、もし Higher-Order component が withSubscription
と名付けられていて、ラップされたコンポーネントの表示名が CommentList
であったとした場合、WithSubscription(CommentList)
という表示名を使ってください。
Caveats – 注意事項
Higher-order components come with a few caveats that aren’t immediately obvious if you’re new to React.
もしあなたが React を始めたばかりであれば、すぐにわかるものではありませんが、Higher-Order components には、いくつかの注意事項があります。
Don’t Use HOCs Inside the render Method: render メソッドの中で HOC を使ってはいけない
React’s diffing algorithm (called reconciliation) uses component identity to determine whether it should update the existing subtree or throw it away and mount a new one. If the component returned from render is identical (===) to the component from the previous render, React recursively updates the subtree by diffing it with the new one. If they’re not equal, the previous subtree is unmounted completely.
React の(reconciliation と呼ばれる)差分検出アルゴリズムは、既存のサブツリーを更新するか、それともそれを破棄して新しいものをマウントするか決定するためにコンポーネントのアイデンティティを使用します。もし、render から返されるコンポーネントが、前回の render
によって返されたコンポーネントと同一(===
)であったとしたら、React は再帰的にサブツリーを新しいものと差分検出することによって更新します。もし、それらが同一でなかった場合、前回のサブツリーは完全にアンマウントされます。
Normally, you shouldn’t need to think about this. But it matters for HOCs because it means you can’t apply a HOC to a component within the render method of a component:
通常は、このことについて考える必要はありません。しかし、これは HOC の問題です。なぜならコンポーネントの render メソッド内のコンポーネントに HOC を適用できないということを意味しているからです。
The problem here isn’t just about performance — remounting a component causes the state of that component and all of its children to be lost.
ここでの問題はパフォーマンスについてのものではありません。コンポーネントを再マウントすることにより、そのコンポーネントの state やその子コンポーネントのすべてが失われてしまうことです。
Instead, apply HOCs outside the component definition so that the resulting component is created only once. Then, its identity will be consistent across renders. This is usually what you want, anyway.
代わりに、HOC をコンポーネント定義の外部に適用してください。結果として生じるコンポーネントは 1 回のみ作られるようになります。それから、そのアイデンティティはレンダリング全体を通して一貫しているでしょう。とにかく、これが一般的に求められることです。
In those rare cases where you need to apply a HOC dynamically, you can also do it inside a component’s lifecycle methods or its constructor.
HOC を動的に適用する必要があるこれらのレアケースにおいて、さらにこれをコンポーネントのライフサイクルメソッドやそのコンストラクタ内で行うことも可能です。
Static Methods Must Be Copied Over: static メソッドは上書きされなくてはいけない
Sometimes it’s useful to define a static method on a React component. For example, Relay containers expose a static method getFragment to facilitate the composition of GraphQL fragments.
時々、React のコンポーネントに static メソッド(静的メソッド)を定義すると役に立つことがあります。たとえば、Relay のコンテナーでは、GraphQL の fragment の作成を容易にするために、static メソッドである getFragment
を公開しています。
When you apply a HOC to a component, though, the original component is wrapped with a container component. That means the new component does not have any of the static methods of the original component.
しかし、HOC をコンポーネントに適用するとき、オリジナルのコンポーネントは Container コンポーネントでラップされます。これは、新しいコンポーネントは、オリジナルのコンポーネントのどんな static メソッドも持っていないということを意味しています。
To solve this, you could copy the methods onto the container before returning it:
これを解決するために、レンダリングする前に、container にメソッドをコピーするとよいでしょう。
However, this requires you to know exactly which methods need to be copied. You can use hoist-non-react-statics to automatically copy all non-React static methods:
しかし、これはどのメソッドがコピーされる必要があるのかしっかり把握しておくことが求められます。自動的にすべての non-React な static メソッドをコピーするために hoist-non-react-statics をお使いください。
Another possible solution is to export the static method separately from the component itself.
もうひとつの可能な解決策は、static メソッドをコンポーネント自身から分離することです。
Refs Aren’t Passed Through: refs は通過させない
While the convention for higher-order components is to pass through all props to the wrapped component, this does not work for refs. That’s because ref is not really a prop — like key, it’s handled specially by React. If you add a ref to an element whose component is the result of a HOC, the ref refers to an instance of the outermost container component, not the wrapped component.
Higher-Order components の作法は、すべての props をラップされたコンポーネントに通過させることですが、一方でこれは refs においては機能しません。これは、ref
は key
と同様に React に特別にハンドリングされるものであり、本当の prop ではないからです。もし ref を HOC の結果として生じるコンポーネントの要素に追加するとしたら、ref はラップされたコンポーネントではなく、もっとも外側のコンテナーコンポーネントのインスタンスを参照します。
The solution for this problem is to use the React.forwardRef API (introduced with React 16.3). Learn more about it in the forwarding refs section.
この問題の解決策は、React.forwardRef
API(React 16.3 で導入されました)を使うことです。詳細について、forwarding refs のセクションでご確認ください。
訳者まとめ
翻訳は以上です。前回訳した Render Props もそうでしたが、なかなかとっつきにくい概念だと思います。一通りドキュメントを読めば、ある程度わかった気持ちになるかと思いますが、特にこの HOC は慣れていないと使いどころが難しいと思いますので、実際にコードを書いて、React を書く際にどのような部分で使えるのかイメージを掴むようにするとよいのではないかと思います。
HOC 自体の考え方は、React の初期の頃からすでにあって、さまざまなライブラリなどで取り入れられています。昨年くらいから Render Props という書き方が話題になるようになり、以下のような記事があるのように、HOC でもまだ解決できていない問題があり、今後は抽象化を行う際は Render Props 的な書き方が主流になっていくと予想されます(私自身は、最近は React の現場にいないので、実感としてその辺がよくわからないのが辛いところです)。実際に Render Props で HOC を書くこともできるようです。
いずれにせよ、HOC だったり、Render Props だったり、関数型的な頭を使う書き方ができるのが、React の面白いところだと思っています。さまざまなケースで使い分けができるように、いろいろ使いこなせるようになっておきたいところですね。
コメント