Reactアプリのコードを美しく保つ「高階コンポーネント」の考え方とは?
2016/08/25
Jack Franklin
Articles in this issue reproduced from SitePoint
Copyright © 2016, All rights reserved. SitePoint Pty Ltd. www.sitepoint.com. Translation copyright © 2016, KADOKAWA CorporationJapanese syndication rights arranged with SitePoint Pty Ltd, Collingwood, Victoria,Australia through Tuttle-Mori Agency, Inc., Tokyo
Reactアプリケーションを整理して構築し、保守を簡単にするためにHigher-Order Componentsを使用する方法について説明します。純粋関数でコードをクリーンに保つ方法や、同じ原理をReactコンポーネントに適用する方法を紹介します。
純粋関数
以下のプロパティに従っている場合、その関数は純粋であると見なされます。
- 取り扱うすべてのデータが引数として宣言されている
- 与えられたデータやその他のデータを変化させない(多くの場合、副作用と呼ばれる)
- 同じ入力には、常に同じ出力を返す
たとえば、以下のadd関数は純粋です。
function add(x, y) {
return x + y;
}
しかし以下のbadAdd関数は純粋ではありません。
var y = 2;
function badAdd(x) {
return x + y;
}
badAdd関数は直接与えられていないデータを参照するので、純粋ではありません。結果として、以下のように同じ入力でこの関数を呼び出すと、異なる出力を得られます。
var y = 2;
badAdd(3) // 5
y = 3;
badAdd(3) // 6
純粋関数についての詳細は、 Mark Brownの「An introduction to reasonably pure programming(適度に純粋なプログラミングの手引き)」を参照してください。
純粋関数は非常に有用で、アプリケーションのデバッグとテストをより簡単にしますが、場合によっては副作用を持つ不純関数を作成したり、(たとえばライブラリーの関数のような)直接アクセスできない既存の関数の挙動に変更を加える必要があります。これらのことを可能にするには、高階関数を検討する必要があります。
高階関数
高階関数は呼び出されたときに別の関数を返す関数です。また、関数を引数として扱うこともよくあります。関数を引数とする場合、高階と見なされる関数は必要ありません。
たとえば、上のadd関数で、add関数を呼び出して結果を返す前にコンソールにログを記録するようにコードを記述したいとします。add関数の編集はできないので、代わりに次のような新しい関数を作成します。
function addAndLog(x, y) {
var result = add(x, y);
console.log('Result', result);
return result;
}
関数のロギング結果が有用であると判断し、今度はsubtract関数と同様の関数にしたいと考えます。上のコードを複製するのではなく、関数を受け取り、与えられた関数を呼び出して結果を返す前に結果をログに記録する新しい関数を返せる、高階関数を記述します。
function logAndReturn(func) {
return function() {
var args = Array.prototype.slice.call(arguments);
var result = func.apply(null, args);
console.log('Result', result);
return result;
}
}
今度はこの関数を受け取り、以下のようにロギングをaddとsubtractに追加するために使用します。
var addAndLog = logAndReturn(add);
addAndLog(4, 4) // 8 is returned, ‘Result 8’ is logged
var subtractAndLog = logAndReturn(subtract);
subtractAndLog(4, 3) // 1 is returned, ‘Result 1’ is logged;
logAndReturnは、それ自身の引数として関数を受け取り、ユーザーが呼び出せる新しい関数を返すので、HOF(Higher-Order Function:高階関数)です。動作を変更できない既存の関数をラップするのに非常に便利です。詳細については、さらに詳しく記載されているM. David Greenの記事「Higher-Order Functions in JavaScript(JavaScriptの高階関数)」を参照してください。
また、上記のコードが動作している例はこちらのCodePenで見られます。
Higher-Order Components
Reactの領域に入ることで、上と同様のロジックを、既存のReactコンポーネントを取得して追加の動作に使用できます。
この項では、Reactの事実上のルーティングソリューションであるReact Routerを使用します。このライブラリーを使い始めるには、GitHubの「React Router Tutorial」が非常にお勧めです。
React RouterのLinkコンポーネント
React Routerは、Reactアプリケーションでページ間のリンクに使用される<Link>コンポーネントを提供しています。<Link>コンポーネントのプロパティの1つがactiveClassNameです。<Link>がactiveClassNameプロパティを持っており現在作動中の場合(ユーザーがリンクの指すURLにいる場合)、<Link>コンポーネントにクラスが与えられ、デベロッパーがスタイルを設定できるようにします。
<Link>コンポーネントにクラスが与えられ、デベロッパーがスタイルを設定できる機能は本当に便利で、架空のアプリケーションでは常にこのプロパティを使用すると決めています。しかし、その後すぐにすべての<Link>コンポーネントが冗長になっていることに気がつきます。
<Link to="/" activeClassName="active-link">Home</Link>
<Link to="/about" activeClassName="active-link">About</Link>
<Link to="/contact" activeClassName="active-link">Contact</Link>
クラス名のプロパティを毎回繰り返さなければならないことに注意してください。この繰り返しはコンポーネントを冗長にするだけでなく、クラス名を変更するときは多くの場所で変更が必要になることも意味します。
その代わりに、<Link>コンポーネントをラップするコンポーネントを記述できます。
var AppLink = React.createClass({
render: function() {
return (
<Link to={this.props.to} activeClassName="active-link">
{this.props.children}
</Link>;
);
}
});
リンクを整頓する以下のコンポーネントを使用できます。
<AppLink to="/">Home</AppLink>
<AppLink to="/about">About</AppLink>
<AppLink to="/contact">Contact</AppLink>
このコンポーネントが動作している例はこちらのPlunkerで見られます。
これらのコンポーネントは、受け取ったあとの既存のコンポーネントを変更することなくわずかに操作するので、ReactエコシステムではHigher-Order Components(HOC:高階コンポーネント)として知られています。ラッパーコンポーネントとしても捉えられますが、一般的にReactベースのコンテンツではHigher-Order Componentsと称されることで分かると思います。
ステートレスな関数コンポーネント
React0.14は、ステートレスな関数コンポーネント(Stateless functional components)のためのサポートを導入しました。これらは、以下のような特徴を持つコンポーネントです。
- 階層がない
- (componentWillMount()などの)Reactのライフサイクルメソッドを使用していない
- renderメソッドのみを定義しており、これ以上のものではない
コンポーネントが上記を着実に実行する場合、React.createClass(あるいはES2015クラスを使用している場合はclass App extends React.Component)を使用する代わりに、関数として定義できます。たとえば、以下の2つの式は両方とも、同じコンポーネントを生成します。
var App = React.createClass({
render: function() {
return
My name is { this.props.name }
;
}
});
var App = function(props) {
return
My name is { props.name }
;
}
ステートレスな関数コンポーネントでは、this.propsを参照するのではなく、propsが引数として渡されます。これについては、Reactのマニュアルで詳細が確かめられます。
Higher-Order Componentsは多くの場合、既存のコンポーネントをラップするので、関数コンポーネントとして定義できます。記事の後半では、可能なかぎり定義していきます。
より良いHigher-Order Components
上のコンポーネントは動作しますが、もっと良くすることもできます。作成したAppLinkコンポーネントは、目的にはあまり沿っていません。
複数のプロパティの受け入れ
<AppLink>コンポーネントは、以下の2つのプロパティを見込んでいます。
- this.props.to:ユーザーを導くリンクのURL
- this.props.children:ユーザに表示されるテキスト
しかし、<Link>コンポーネントはより多くのプロパティを受け入れますし、ほぼ常に渡さなければならない上の2つのプロパティとともに、追加のプロパティを渡したいときがあるかもしれません。必要とする正確なプロパティをハードコーディングすることによって<AppLink>を拡張できるようにはまったくしていません。
JSXスプレッド
React要素を定義するために使用するHTMLのような構文のJSXは、プロパティとしてオブジェクトをコンポーネントに渡すためのスプレッド演算子をサポートしています。たとえば、次のコードサンプルでは、同じことをしています。
var props = { a: 1, b: 2};
<Foo a={props.a} b={props.b} />
<Foo {...props} />
{…props}を使用してオブジェクトの各キーを展開し、個々のプロパティとしてFooに渡します。
<Link>をサポートしている任意のプロパティをサポートしているように、<AppLink>でもこのトリックを利用できます。また、将来も使い続けられるようにします。今後<Link>が新しいプロパティを追加する場合、ラッパーコンポーネントがすでにサポートしていることになります。一方で、私はAppLinkのファンクショナルコンポーネントへの変更もするつもりです。
var AppLink = function(props) {
return <Link {...props} activeClassName="active-link" />;
}
これで<AppLink>は任意のプロパティの受け入れと受け渡しをします。また、<Link>タグの間で{props.children}を明示的に参照する代わりに、自分自身を閉じるフォームが使用できることにも注意してください。Reactでは、通常のpropとして、または開閉タグの間のコンポーネントの子要素としてchildrenを渡せます。
動作している例はこちらのPlunkerで確認できます。
Reactでのプロパティの順序
ページ上の特定のリンク1つのために、別のactiveClassNameを使用しなければならないと想像してください。すべてのプロパティを以下のコードサンプルを通して渡しますので、activeClassNameを<AppLink>に渡してみましょう。
<AppLink to=“/special-link” activeClassName=“special-active”>Special Secret Link</AppLink>
しかし、これは動作しません。理由は<Link>コンポーネントをレンダリングするときのプロパティの順序のせいです。
return <Link {...props} activeClassName="active-link" />;
Reactコンポーネントに同じプロパティが複数回ある場合、最後の宣言が優先されます。つまり、{…this.props}の後に記述されているactiveClassName=“active-link”宣言が常に優先されます。この問題を解決するには、this.propsを最後に展開するようにプロパティを並べ替えることです。これは使用したい適切なデフォルト値を設定していることを意味しますが、本当に必要ならばユーザーは次のようにもできます。
return <Link activeClassName="active-link" {...props} />;
動作の変更はこちらのPlunkerで確認できます。
既存のコンポーネントをラップしながら追加の動作をともなう、Higher-Order Componentを開発して、コードベースをクリーンに保ち、プロパティを繰り返さず1つの場所で値を保持して将来予想される変更から保護します。
Higher-Order Componentのクリエイター
多くの場合、同じ動作でラップする必要のあるたくさんのコンポーネントが存在します。ロギングを追加するためにaddとsubtractをラップするという、記事の前半で述べたことによく似ています。
システム上で認証されている現在のユーザーに関する情報を含むオブジェクトがアプリケーションの中にあると想像してください。この情報にアクセスできるようにするいくつかのReactコンポーネントが必要ですが、すべてのコンポーネントをやみくもにアクセス可能にするより、どのコンポーネントが情報を受け取るかについて、より厳しくしたいはずです。
解決する方法は、Reactコンポーネントを呼び出せる関数を作成することです。作成した関数は、指定されたコンポーネントをレンダリングするだけでなく、ユーザー情報へのアクセス権を与える追加のプロパティをともなう新しいReactコンポーネントを返します。
かなり複雑に聞こえますが、次のようないくつかのコードで簡単に作れます。
function wrapWithUser(Component) {
// information that we don’t want everything to access
var secretUserInfo = {
name: 'Jack Franklin',
favouriteColour: 'blue'
};
// return a newly generated React component
// using a functional, stateless component
return function(props) {
// pass in the user variable as a property, along with
// all the other props that we might be given
return <Component user={secretUserInfo} {...props} />
}
}
関数は、文字列の最初を大文字にしなければならない指定のReactコンポーネントに簡単に目印をつけるReactコンポーネントを受け取り、secretUserInfoに設定されているuserの追加のプロパティで与えられたコンポーネントをレンダリングする新しい関数を返します。
今度は、ログインしているユーザーを表示できるように、この情報へのアクセスを必要とするコンポーネント<AppHeader>を説明します。
var AppHeader = function(props) {
if (props.user) {
return <p>Logged in as {props.user.name}</p>;
} else {
return <p>You need to login</p>;
}
}
最後のステップは、this.props.userを受け取るようにこのコンポーネントを接続することです。 this.props.userをwrapWithUser関数に渡すことで新しいコンポーネントを作成できます。
var ConnectedAppHeader = wrapWithUser(AppHeader);
これで、レンダリング可能な<ConnectedAppHeader>コンポーネントを持ち、userオブジェクトにアクセスできるようになります。
実際の動作はこちらのCodePenで確認できます。
接続されているいくつかの追加データには、すべてのコンポーネントがアクセスできるわけではないので、コンポーネントConnectedAppHeaderを呼び出すことにしました。
ConnectedAppHeaderを呼び出すパターンはReactライブラリー、特にReduxで非常に一般的なので、どのように動作するかと使用されている理由を知っていれば、アプリケーションの成長と、このアプローチを使用したサードパーティ製ライブラリーの信頼に役立ちます。
最後に
この記事では、純粋関数やHigher-Order Componentsなどの関数型プログラミングの原則をReactに適用することで、どのようにすればメンテナンスしやすく、使い続けられるコードを簡単に書けるのかを説明しました。
定義されたデータを1か所のみで保持できるHigher-Order Componentsを作成することで、リファクタリングを容易にします。高階関数の作成者は、データをプライベートに保つことを可能にし、本当に必要とするコンポーネントに対してのみデータを部分的に公開できるようにしています。そして、どのコンポーネントがどのデータを使用しているかが明らかになります。これらのことはアプリケーションが成長するにつれ、有益であることが分かってくるでしょう。
※本記事は、ゲストオーサーのJack Franklinによるものです。SitePointのゲスト投稿では、JavaScriptコミュニティの著名な執筆者や講演者の魅力的なコンテンツの提供を目指しています。
(原文:Higher Order Components: A React Application Design Pattern)
[翻訳:柴田理恵]
[編集:Livit]
Copyright © 2016, Jack Franklin All Rights Reserved.
Jack Franklin
ロンドンで働くJavaScriptとRubyの開発者です。ツール作成、ES2015、ReactJSに重点的に取り組んでいます。