9 things every React.js beginner should knowを意訳しました。
誤りやより良い表現などがあればご指摘頂けると助かります。
私は約6ヶ月間React.jsを使用してきました。それほど長い歴史ではありませんが、あなたがひげの長老として扱われるようなJavaScriptフレームワークの目まぐるしい世界の大きな枠組みの中で、私は最近、React初学者のTipsで少数の人々を支援してきましたので、ここでより多くの人々にその内容を共有するのが良いアイデアであると思いました。これらは全て私が始めた時に知っておきたかったことか、もしくはReactを習得するために本当に役立ったもののいずれかです。
あなたが絶対的な基本を知っていると想定して話を進めますが、もしコンポーネント、propsやstateなどの言葉に馴染みがなければ、公式の入門やチュートリアルページを読むとよいでしょう。JSXもまた利用しますが、コンポーネントを記述するのにはるかに簡潔かつ表現力豊かな構文であるためです。
1. ビューだけを管理するライブラリ
基本を学んで行きましょう。Reactは他のMVCフレームワークとは異なり、ビューを描画するためだけのライブラリです。フロントエンドMVCでの開発経験があれば、Reactは単なる「V」であり、「M」と「C」については他の場所で管理する必要があることが分かるでしょう。さもなければ、気持ち悪いReactのコードになってしまうでしょう。詳細は後述します。
2. コンポーネントは小さく保つ
自明であるように見えるかも知れませんが、強調する価値があります。全ての良い開発者は、クラス、モジュール、その他は小さい方が理解しやすく、テストやメンテナンスもまた容易になることを知っていますが、Reactコンポーネントでもまた同様です。React開発を始めた当初、コンポーネントがどれだけ小さくあるべきかを過小評価していたことは失敗でした。もちろん、正確なサイズは多くの異なる要因(あなたやチーム固有の好みを含む!)次第ですが、一般的なアドバイスとして、あなたが思うよりもはるかに小さくしておくことをオススメします。例として、私のウェブサイトのトップページにある最新のブログエントリーを表示するコンポーネントを示します。
const LatestPostsComponent = props => (
<section>
<div><h1>Latest posts</h1></div>
<div>
{ props.posts.map(post => <PostPreview key={post.slug} post={post}/>) }
</div>
</section>
);
コンポーネント自身は内部に2つの <div>
を持った <section>
です。最初の子要素はヘディングを持ち、2番目の子要素はいくつかのデータをマッピングして複数の <PostPreview>
コンポーネントとして描画します。 <PostPreview>
をそれ自身のコンポーネントとして抽出する最後の部分は大事なポイントです。これがコンポーネントの適切なサイズです。
3. 関数型コンポーネントを使う
以前は、Reactコンポーネントを定義するのに2つの選択肢がありました。最初の1つは React.createClass()
を利用する方法です。
const MyComponent = React.createClass({
render: function() {
return <div className={this.props.className}/>;
}
});
もう一方はES6クラスを利用する方法です。
class MyComponent extends React.Component {
render() {
return <div className={this.props.className}/>;
}
}
React 0.14はコンポーネントをpropsの関数として定義する新しい構文を紹介しました。
const MyComponent = props => (
<div className={props.className}/>
);
これはReactコンポーネントを定義するのに断然お気に入りの方法です。より簡潔な構文は別として、このアプローチは、コンポーネントが分割を必要とする時にそれを明示するのに本当に役立ちます。先ほどの例をもう一度確認し、まだ分割されていないと想像してみましょう。
class LatestPostsComponent extends React.Component {
render() {
const postPreviews = renderPostPreviews();
return (
<section>
<div><h1>Latest posts</h1></div>
<div>
{ postPreviews }
</div>
</section>
);
}
renderPostPreviews() {
return this.props.posts.map(post => this.renderPostPreview(post));
}
renderPostPreview(post) {
return (
<article>
<h3><a href={`/post/${post.slug}`}>{post.title}</a></h3>
<time pubdate><em>{post.posted}</em></time>
<div>
<span>{post.blurb}</span>
<a href={`/post/${post.slug}`}>Read more...</a>
</div>
</article>
);
}
}
クラスとして見る限り、このクラスはそう悪くはありません。既に1組のメソッドをrenderメソッドから抽出し、小さな単位に分割し、良い名前を付けました。さらに、最新の投稿のプレビューを描画するアイデアを良い感じにカプセル化しました。同じコードを関数型構文を使って書き直してみましょう。
const LatestPostsComponent = props => {
const postPreviews = renderPostPreviews(props.posts);
return (
<section>
<div><h1>Latest posts</h1></div>
<div>
{ postPreviews }
</div>
</section>
);
};
const renderPostPreviews = posts => (
posts.map(post => this.renderPostPreview(post))
);
const renderPostPreview = post => (
<article>
<h3><a href={`/post/${post.slug}`}>{post.title}</a></h3>
<time pubdate><em>{post.posted}</em></time>
<div>
<span>{post.blurb}</span>
<a href={`/post/${post.slug}`}>Read more...</a>
</div>
</article>
);
クラスの中にメソッドを持つ代わりに、裸の関数を持つことを除けばほぼ同じコードです。しかし私にとっては大きな違いがあります。クラスベースの例では、私の視線は class LatestPostsComponent {
に向き、自然に閉じ中括弧を探して下がり、「クラスの終わりだからコンポーネントの終わりだな」と結論づけます。「でも待てよ、同じモジュール内でコンポーネントの外にある他のコードは何だろう?なるほど!データを渡してビューを描画するための別の関数だな!それを取り出してコンポーネント自身に含めることができるな!」
関数型コンポーネントがポイント#2を識別するのに役立つのと、どちらが冗長な方法でしょうか?
Reactの将来的な最適化の余地もあり、関数型コンポーネントはクラスベースコンポーネントよりも効率的になるでしょう。(更新: 関数型コンポーネントのパフォーマンスの実装は思ったよりも複雑なことが分かりました。今でも標準で関数型コンポーネントを書くことをオススメしますが、パフォーマンスが大きな問題になる場合はこれとこれを読み、あなたにとって最適な選択をしてください。)
関数型コンポーネントがいくつかの「制限」を持つことに注意してください。私はそれを最大の強みと考えています。まず、関数型コンポーネントは ref
を割り当てることができないということです。 ref
はコンポーネントが子要素を「探す」のに役立つ一方で、私はこれはReactを書く上での間違った方法であると考えています。 ref
属性はほとんどjQueryライクなコンポーネントの書き方には必要不可欠ですが、これは最初にReactを選択した理由である、関数型の単一方向データフローという哲学から私たちを遠ざけます!
もう1つの大きな違いは、関数型コンポーネントはstateを持つことができないという点で、これは大きな利点です。なぜなら次のTipsは...
4. ステートレスなコンポーネントを書く
Reactベースのアプリケーションを書くときに感じる多くの、大多数の苦しみは、多すぎるstateから来ると言っても間違いないでしょう。
stateはコンポーネントをテストしづらくする
ピュア、つまりデータを入力してデータを出力するだけの関数よりも容易にテストできるものは実質的にないのにも関わらず、何故コンポーネントにそのようなstateを加える機会を捨て去らないのでしょうか?ステートフルなコンポーネントをテストする際、それらの振る舞いをテストするために、コンポーネントに「正しいstate」を適用する必要があります。state(コンポーネントがいつでも操作できる)とprops(コンポーネントが制御できない)の様々な組み合わせを洗い出し、そのうちどれをテストするか、またどのように行うかを理解する必要もあります。コンポーネントがpropsを入力するピュアな関数である場合、テストはずっと簡単になります。(テストについては後述)
stateはコンポーネントを推測しづらくする
非常にステートフルなコンポーネントを読む際、起こっている全てのことを追跡するには相当な苦労を強いられます。いくつか言及すると、「そのstateはもう初期化された?」、「ここでこのstateを変化させたら何が起こる?」、「このstateを変更する箇所はいくつある?」、「このstateに競合状態はある?」など、すべて一般的に良くある質問です。ステートフルなコンポーネントを追跡し、それらがどのように振る舞うかを理解することは大変な苦痛になります。
stateはビジネスロジックをコンポーネント内に置くことを容易にする
振る舞いを決めるためにコンポーネントを検索するようなことはするべきではありません。コンポーネントのrenderロジックがOKである一方、ビジネスロジックが巨大なコードの匂いがするように、Reactがビューのライブラリであることを忘れないでください。しかし、あなたのアプリケーションのコンポーネント内のstateの多くが正しい時、つまり this.state
で簡単にアクセスできる時、stateが属していないコンポーネント内に計算やバリデーションのようなものを設置したくなる誘惑に駆られます。以前のポイントにある通り、この考えによりテストがずっと困難になることを肝に銘じてください。つまり、邪魔になるビジネスロジックなしにテストすることができなくなり、テストには漏れ無く邪魔なビジネスロジックがついてきます。
stateはアプリケーションの別パーツに情報を共有することを困難にする
コンポーネントがstateの一部を持つ時、コンポーネントの階層を下ってそれを渡すのは簡単ですが、他の方向では注意が必要です。
もちろん、コンポーネントがstateの一部を持つのが理に適うこともあります。そのようなケースでは、 this.setState
を使ってください。ReactコンポーネントAPIに従っているので、使用不可であるという印象は与えたくありません。たとえば、ユーザーがフィールドに入力する際、全てのキー入力をアプリケーション全体に公開するのは意味がないため、そのフィールドはblurイベント、つまり、最後の入力が外部に送信されて他の場所に格納されるまでは中間stateを保持するかもしれません。このシナリオは最近同僚から教えてもらったのですが、素晴らしい事例だと思います。
コンポーネントにstateを加える時にはとにかく用心するようにしてください。一旦始めてしまうと、「もう1つだけ」加えることは簡単ですし、あなたが気づく前に制御不能になります!
5. Redux.jsを使う
ポイント#1でReactはビューのためのものであると言いました。その時の明らかな疑問は、「stateとロジックはどこに置くべきか?」です。ご質問頂きありがとうございます!
Webアプリケーション設計時のスタイル、パターン、アーキテクチャであり、広くReactの描画に使われることの多いFluxのことを既にご存知かもしれません。Fluxの概念を実装するフレームワークはいくつかありますが、間違いなくRedux.jsをオススメします。
Reduxの機能や長所について丸ごと独立したブログ記事を書くことを考えていますが、今のところは上のリンクにある公式チュートリアルを読むと良いでしょう。加えて、Reduxがどのように動作するかの非常に簡潔な説明を記述します。
- コンポーネントはUIイベントが起こった時に呼び出すpropsとしてコールバック関数を受け取る
- これらのコールバック関数は、イベントベースでactionsをcreateしてdispatchする
- reducersは新しいstateを計算してactionsを処理する
- アプリケーション全体の新しいstateは単一のstore
になる - コンポーネントは新しいstateをpropsとして受け取り必要な場所で自身を再描画する
上記の多くはRedux固有の概念ではありませんが、Reduxはそれらを小さなAPIで非常にクリーンかつシンプルな方法で実装しています。まともなサイズのプロジェクトをAlt.jsからReduxに切り替えましたが、私が発見した大きな利点のいくつかは次の通りです。
- reducersは単純に
oldState + action = newState
を処理するピュアな関数です。それぞれのreducerはstateの一部を計算し、全てを組み合わせてアプリケーション全体を構築します。これはあなたの全てのビジネスロジックとstateの遷移のテストが本当に簡単になる - APIは小さく、シンプルで、よくまとまったドキュメントがある。全ての概念を学ぶを簡単に素早く学ぶことができるため、私自身のプロジェクトのactionsと情報の流れを理解するのが楽だった
- 推奨される方法で使用すると、非常に少ないコンポーネントのみがReduxに依存し、他の全てのコンポーネントはstateとコールバックをpropsとして受け取るだけになるため、コンポーネントは非常にシンプルになり、フレームワークのロックインを軽減する
非常に巧妙にReduxを補完する他のライブラリがいくつかあるので、ぜひ使ってみてください。
- Immutable.js - JavaScriptのための不変のデータ構造!stateをそれらに補完することで、予期せぬデータの変更を防ぎ、reducerをピュアに保つ
- redux-thunk - actionsがアプリケーションのstateを更新する以外の副作用を持つ必要がある場合に使われる。例えば、REST APIを呼んだり、ルーティングや他のactionsをdispatchするなど
- reselect - state内で構成可能な遅延評価されたビューのために使う。例えば、特定のコンポーネントに下記のような処理を行いたい時
- グローバルなstateツリーに全体ではなく関連部品だけを注入する
- 合計値やバリデーションstateのような導出された余計なデータをstoreに置くことなく注入する
必ずしも初日からこれら全てを必要とはしません。ReduxとImmutable.jsはstateを持つようになったら、reselectは導出されたstateを持つようになったら、redux-thunkはルーティングや非同期アクションを持つようになったら、できるだけ早く追加しておきたいところです。すぐにでもそれらを追加すると、後で追加導入する労力を節約できます。
- 尋ねる人によっては、Reduxは「真の」Fluxであったりなかったりするかもしれません。個人的には、その中心概念をFluxフレームワークと呼ぶのは十分に良く整理されていると感じますが、いずれにしても、論点は意味的なものです。
6. 常にpropTypesを使う
propTypesによってコンポーネントにちょっとした型安全性を実に簡単に提供できます。これは次のようになります。
const ListOfNumbers = props => (
<ol className={props.className}>
{
props.numbers.map(number => (
<li>{number}</li>)
)
}
</ol>
);
ListOfNumbers.propTypes = {
className: React.PropTypes.string.isRequired,
numbers: React.PropTypes.arrayOf(React.PropTypes.number)
};
開発環境(本番環境でない)において、必須propが与えられなかったり、誤った型のpropsがが与えられた場合、Reactはログ上でエラーを知らせます。これにはいくつかの利点があります。
- 馬鹿げたミスを防ぐことで、バグを早期にに発見できる
-
isRequired
を使うことで、何度もundefined
やnull
チェックをする必要がなくなる - コンポーネントに必要なpropsを探す手間を省くためのドキュメント代わりになる
上記のリストは動的型付けよりも静的型付けを提唱しているように見えるかもしれません。個人的には、開発時の簡便性とスピードの点で動的型付けの方が気に入っているのですが、propTypesは複雑なことをせず、Reactコンポーネントに多くの安全性を提供してくれることに気づきました。率直に言って、どんな時でもこれを利用しない手はありません。
最後のTipはテストでいかなるpropTypesエラーも失敗するようにすることです。以下は少々ぶっきらぼうではありますが、シンプルで実際に動作します。
beforeAll(() => {
console.error = error => {
throw new Error(error);
};
});
7. シャローレンダリングを使う
Reactコンポーネントのテストは少々扱いにくい話題です。難しいからというわけではなく、まだまだ発展途上の領域であり、ベストプラクティスとしての手法が確立していないためです。現時点でのオススメは、シャローレンダリングとpropのアサーションを使う方法です。
シャローレンダリングは、1つのコンポーネントを完全に描画することができるという点で優れていますが、子コンポーネントの描画まで掘り下げることはしてくれません。代わりに、出力されるオブジェクトは子要素の型とpropsを教えてくれます。これにより上手く疎結合が保たれ、一度に1つのコンポーネントのテストが可能になります。
コンポーネントのユニットテストには広く使われる3つのタイプがあります。
描画ロジック
条件により画像もしくはローディングアイコンのいずれかが表示されるコンポーネントを想像してください。
const Image = props => {
if (props.loading) {
return <LoadingIcon/>;
}
return <img src={props.src}/>;
};
このようにテストできるでしょう。
describe('Image', () => {
it('renders a loading icon when the image is loading', () => {
const image = shallowRender(<Image loading={true}/>);
expect(image.type).toEqual(LoadingIcon);
});
it('renders the image once it has loaded', () => {
const image = shallowRender(<Image loading={false} src="https://example.com/image.jpg"/>);
expect(image.type).toEqual('img');
});
});
簡単ですね!シャローレンダリングのAPIは先に示したものよりも少し複雑であることを指摘せねばなりません。上記で使われた shallowRender
関数は使いやすくするために本当のAPIをラップした自作のヘルパーです。
先ほどの ListOfNumbers
コンポーネントに戻ると、mapが正しく実行されているかをテストする方法が記述されています。
describe('ListOfNumbers', () => {
it('renders an item for each provided number', () => {
const listOfNumbers = shallowRender(<ListOfNumbers className="red" numbers={[3, 4, 5, 6]}/>);
expect(listOfNumbers.props.children.length).toEqual(4);
});
});
propの変換
最後の事例では、テストされたコンポーネントの子要素まで正しく描画されていることを確認するために深堀りしました。子要素があるかどうかだけでなく、正しいpropsが与えられたかどうかもアサートすることでこれを拡張することができます。コンポーネントがpropsの中でそれらを渡す前に変換する時に特に役立ちます。例えば、次のコンポーネントは文字列型の配列としてCSSクラス名を保持しており、それらをスペースで区切られた単一の文字列として渡します。
const TextWithArrayOfClassNames = props => (
<div>
<p className={props.classNames.join(' ')}>
{props.text}
</p>
</div>
);
describe('TextWithArrayOfClassNames', () => {
it('turns the array into a space-separated string', () => {
const text = 'Hello, world!';
const classNames = ['red', 'bold', 'float-right'];
const textWithArrayOfClassNames = shallowRender(<TextWithArrayOfClassNames text={text} classNames={classNames}/>);
const childClassNames = textWithArrayOfClassNames.props.children.props.className;
expect(childClassNames).toEqual('red bold float-right');
});
});
テストにおけるこのアプローチに対するよくある批判の1つは、 props.children.props.children
...の急増です。それは最適なコードとは言えませんが、個人的には1つのテストで props.children
を書き過ぎることに悩まされていることに気づきました。それはコンポーネントが大きすぎたり、複雑すぎたり、深くネストされすぎたり、また分割されるべきサインです。
もう一方のよく聞く批判は、テストがコンポーネントの内部実装に依存しすぎるため、わずかなDOM構造の変更だけで全てのテストが壊れるということがあります。これは間違いなくフェアな批判であり、脆いテストスイートは誰も望まないものです。これを上手く管理するベストな方法は、コンポーネントを小さくシンプルに保ち、1つのコンポーネントの変更で壊れるようなテストの数を制限することです。
ユーザーインタラクション
もちろん、コンポーネントは表示するためだけでなく、インタラクティブでもあります。
const RedInput = props => (
<input className="red" onChange={props.onChange} />
)
これらをテストするお気に入りの方法は次の通りです。
describe('RedInput', () => {
it('passes the event to the given callback when the value changes', () => {
const callback = jasmine.createSpy();
const redInput = shallowRender(<RedInput onChange={callback}/>);
redInput.props.onChange('an event!');
expect(callback).toHaveBeenCalledWith('an event!');
});
});
簡単な例ですが、理解してくれることを願います。
統合テスト
これまでのところ、単独のユニットテストだけを説明してきましたが、アプリケーションが正常に接続し、実際に動作することを保証するためにより高レベルのテストも欲しくなることでしょう。ここでは詳細は省きますが、基本的には
- コンポーネントの完全なツリーを描画する(シャローレンダリングの代わりに)
- 最も気にしている要素をみつけるためにDOM(React TestUtilsやjQueryを使う)を取得して
- HTMLの属性や内容をアサートするか
- DOMイベントをシミュレートしてから副作用(DOMもしくはルート変更、Ajax呼び出しなど)をアサートする
テスト駆動開発
一般に、Reactコンポーネントを書くときにはテスト駆動開発はしません。
コンポーネント上で作業している時、最もシンプルなHTMLとサポート対象のどんなブラウザでも正しく表示されるような正しいCSSを書こうとするあまり、その構造をかなりかき回しがちなことに気付きました。コンポーネントのユニットテストの手法はコンポーネントの構造をアサートしがちなため、テスト駆動開発はDOMを微調整する際に一貫してテストの修正を引き起こすでしょうが、それは時間の無駄に思えます。
もう一方の要因は、テストファーストの利点が減少するようにコンポーネントはシンプルであるべきということです。全ての複雑なロジックと変換はaction creatorsとreducersに引き出されますが、これはテスト駆動開発の利点を享受できるポイントです。
これはテストについての最後のポイントをもたらします。このセクション全体で、私はコンポーネントのテストについて扱ってきましたが、Reduxベースの残りのアプリケーションのテストには特別な情報が必要とされないためです。フレームワークとして、Reduxはこっそり動作するとても小さな「魔法」を持ちますが、これは過剰なモックや他のテスト用ボイラープレートを減らすのに役立ちます。全ては古い関数(多くはピュアな)に過ぎず、テストとなると本当に新鮮な空気の一息となります。
8. JSX、ES6、Babel、Webpackとnpmを使う
ここでReact固有のものはJSXです。JSXが手動で React.createElement
を呼ぶよりも優れていることは言うまでもありません。唯一の欠点は、ビルド時の複雑さが若干増えることくらいですが、これは、Babelによって容易に解決できます。
いったんBabelを追加すれば、定数、アロー関数、デフォルト引数、配列およびオブジェクトの分割代入、配列展開および可変長引数、テンプレートリテラル、イテレータおよびジェネレータ、適切なモジュールシステムなど、素晴らしいES6の機能を使い尽くさない理由はありません。皆さんがツールをセットアップする時間を少々用意しているように、JavaScriptは最近本当に「成長」し始めているように感じられます。
コードをバンドルするwebpack、パッケージマネージャであるnpmで締め括ることで、完全にJavaScriptの流行語に準拠することになります。
9. ReactおよびReduxの開発者ツールを使う
ツールに関して言うと、ReactおよびReduxの開発者ツールは実に素晴らしい出来です。React開発者ツールはReact要素の描画ツリーを調査し、ブラウザ上でどのように見えるかを確認するのに驚くほど役立ちます。Redux開発者ツールはさらに印象的で、発生した全てのaction、引き起こされたstateの更新、さらに時間を遡る能力さえ与えてくれます!これは開発用の依存関係もしくはブラウザ拡張として追加することができます。
コードをセーブすると同時にページが更新されるホットモジュールリプレースメント(ブラウザのリロードが不要)をwebpackで設定することも出来ます。
以上!
このドキュメントがあなたのReactライフにとって幸先の良いスタートのきっかけとなり、よくある失敗を避ける助けになることを願っています。この記事を気に入って頂けたなら、TwitterをフォローするかRSSフィードを購読すると良いかもしれません。
読んでくれてありがとうございます!