2016年9月3日に東京電機大学で開催された「HTML5 Conference 2016」のセッションを特集する第二弾は筆者である私、小林徹が登壇した「 Reactの最新動向とベストプラクティス 」の内容を解説します。
Reactの現状
Reactは、2013年にFacebookが公開した、Viewを作るためのJavaScriptのライブラリーです。
現在のバージョンはv15.3.2です。
v1.0.0からv14.0.0までのバージョンはありません。
「すでに安定していてプロダクションでも利用できる」ことや「セマンティック・バージョニング(パッチ.マイナー.メジャー)に準拠している」ことを示すために、v0.14.8からのバージョンアップ時にv0.15.0ではなく、マイナーのバージョン番号をメジャーにシフトさせてv15.0.0になりました。
Reactの採用事例を見ていきましょう。
Reactを開発しているFacebookは、ウェブページのコメント欄などで部分的にReactを使用しています。 同じくFacebookが運営しているInstagramでは、ウェブページ全体をReactのComponentで構築しています。
Facebook以外の事例を見てみると、NetflixやTwitter、Airbnb、Uberなど多くの企業でReactが使われています。 日本でも、Abema.tvなど大規模にReactが導入されている事例が増えています。
これらは、ReactのDeveloper Toolsを使用することで確認できます。
また、Electron製のターミナルソフトであるHypertermは、ReactとReduxを使って作られています。 機能拡張の仕組みを、Reduxのミドルウェアの仕組みで実現するなど興味深い点も多く、ソースコードはGitHubで公開されています。
他にもJenkinsが、新しいUI ComponentをReactのComponentとして提供しているなど、Reactはまさに「今使われているライブラリー」であることが言えます。
特徴
それではまず最初に、Reactの特徴について見ていきましょう。 Reactのサイトのトップには、「Declarative」、「Component-Based」、「Learn Once, Write Anywhere」の3つのキーワードが紹介されています。
「Learn Once, Write Anywhere(一度学べば、どこでも書ける)」は、ReactにとってDOMはreact-domによるただの1つの環境であることを表しています。
react-nativeなどを使うことで、「同じReactの考え方」でiOSやAndroidのような、DOM以外の環境をターゲットにしたアプリケーションを開発できます。
「Write Once, Run Anywhere(一度書けば、どこでも実行できる)」ではないのは、Viewは各プラットフォームのルールや特徴に応じて構築するべきであり、安易に共通化すべきでないことを意味しています。
しかしながらViewに関連しないロジックの多くは共通化できるとしており、実際にFacebookのAds Managerのアプリは、react-nativeを使うことでiOSとAndroidで80%以上のコードが共通化できているそうです。
それでは、DeclarativeとComponent-Basedについて見ていきます。
Declarative
Reactでは、Declarative(宣言的)にComponentを組み合わせてViewを構築します。 宣言的という言葉の通り、状態に対しての「あるべき姿」を宣言するように定義します。
例えば、イベントに対する処理を命令的に記述すると、下記のようになります。
// elに対する変化を書いている button.on(‘click’, () => el.append(child));
これを宣言的に書くと、下記のようになります。
// render関数は、渡されたstateに対するあるべき表示を定義している
render = state => {
el.innerHTML = state.map(child => <div>${child}</div>).join(‘’);
};
// 状態を更新して、render関数に渡す
button.on(‘click’, () => {
state.push(child);
render(state);
};
上記の場合、render関数は受け取ったstateをもとにViewを構築するだけです。
これは、初期表示の場合もイベントによる更新時も常に同じです。
この結果、render関数はstateのみに依存し、同じstateからは常に同じViewを構築します。
これは、テストやデバッグを容易にします。
ReactはComponentを使い、宣言的にViewを構築します。
const View = props => (
<div>{props.items.map(item => <div>{item}</div>)}</div>
);
Component-Based
Reactでは、Componentを作り、組み合わせることでViewを構築します。 Componentは、Appのようなアプリケーションを全体を表すものから、TextBoxのような既存の要素を少し拡張するようなものまで規模は様々です。
class App extends React.Component {
constructor(...args) {
super(...args);
this.state = {
text: '',
};
}
render() {
return (
<TextBox
text={this.state.text}
onChange={text => this.setState({text})}
/>
);
}
}
const TextBox = (text, onChange) => (
<div>
<input type="text" onChange={e => onChange(e.target.value} />
<p>{text}</p>
</div>
);
ReactDOM.render(
<App />,
document.getElementById('app')
);
Componentは、View = Component(State)のような、Viewを作る関数として考えることができます。
Componentが返すViewは、ReactElementのツリーとなります。
作成したReactElementのツリーは、ReactDOMによってDOMへと反映されます。
更新時は、更新前のReactElementのツリーと比較して、差分だけがDOMに反映されます。
これがVIRTUAL DOMと呼ばれる部分です。
この差分の反映により、Reactは関数のようにViewの構築を宣言的に書いた場合でも、パフォーマンスの劣化を避けることができます。
先ほど紹介したReactを使わないinnerHTMLによる反映では、毎回DOMを再構築するため、規模が大きくなるとパフォーマンス上問題となります。
Componentの作り方
React Componentの作成方法には、下記の4種類あります。
- Stateless Functional Components
- React.Component
- React.PureComponent
- React.createClass
Stateless Functinal Componenens
関数によるComponentの定義方法です。
const Item = ({item}) => (
<div>
<div>{item.name}×{item.count}</div>
</div>
);
ただの関数であり、インスタンスも持たないのでStateやライフサイクルメソッドが定義できないなどの制限があります。 ですが最もシンプルな定義方法であり、Componentを作成する際は、まず最初にStateless Functional Componentsで定義できないかを考えることをオススメします。
SFCと省略して記述されることもあります。
React.Component
ComponentにStateを持たせたい場合や、ライフサイクルメソッドを使いたい場合にはReact.Componentを使った定義方法を使います。
class App extends React.Component {
constructor(...args) {
super(...args);
this.state = {
user: null,
};
}
componentDidMount() {
fetch('/api/user')
.then(res => res.json())
.then(user => this.setState({user}))
;
}
render() {
if (this.state.user == null) return <Loading />;
return (
<div>
<User user={this.state.user} />
</div>
);
}
}
React.PureComponent
パフォーマンスが問題となるような場面では、React.Componentの代わりにReact.PureComponentの利用を検討します。 React.PureComponentを使うことで、PropsとStateに対してのshallowEqual(浅い比較)がshouldComponentUpdateに適用されるようになります。
shouldComponentUpdateは、無駄なrenderメソッドの呼び出しを避けるためのライフサイクルメソッドです。
class Counter extends React.PureComponent {
constructor(...args) {
super(...args);
this.state = {count: 0};
}
render() {
return (
<div>
<p>{this.props.label}:{this.state.count}</p>
<button
onClick={() => this.setState({count: this.state.count + 1})
/>
</div>
);
}
}
React.PureComponentは、PropsやStateのデータをイミュータブルに扱っている場合に利用できます。
イミュータブルであることが保証出来ない場合は、React.Componentを使い、shouldComponentUpdateを自ら実装する必要があります。
ただし、最適化はreact-addons-perfを使い、問題となっている部分を計測して確認してから行うことをオススメします。
React.creteClass
React.createClassは、最初から存在するComponentの作成方法です。 ですが、今後は緩やかに非推奨の方向に進んでいます。
Componentの拡張
Reactでは、Componentの拡張を継承ではなく、Higher Order Components(HOC)を使ったCompositionで行うパターンが推奨されています。
Higher Order Componentsとは、高階関数(Higher Order Function)のComponent版と考えることができます。 Higher Order Functionは関数を引数や戻り値とする関数です。
https://ja.wikipedia.org/wiki/高階関数
- Higher Order Functionの例
// 何かする関数を受け取って、結果をログ出して返す
const logger = operation => (...args) => {
const result = operation(...args);
console.log(result);
return result;
};
const add = logger((a, b) => a + b);
add(10, 20);
// 30
Higher Order Componentsでは、Componentを引数として受け取り、それをラップした新しいComponentを返します。
- Higher Order Componentsの例
// Componentを受け取って、PureComponentでラップして返す
const pure = Component => (
class Pure extends React.PureComponent {
render() {
return <Component ...{this.props}/>;
}
}
);
const Item = ({name}) => <div>{name}</div>;
const PureItem = pure(Item);
// <PureItem name=“foo” />
Higher Order Componentsのパターンは、react-routerやreact-reduxなどのライブラリーでも使われています。
また、Higher Order Componentsのパターンを集めたrecomposeというライブラリーもあります。
// isLoadedによってComponentを出し分ける
const enhance = branch(
props => props.isLoaded,
Component => Component,
() => Loading
);
const LoadUser = enhance(User);
// <LoadUser isLoaded={isLoaded} user={user} />
State管理
Reactでは、ComponentはView = Component(State)であると紹介しました。
したがって、Componentにはなるべく状態を持たせないことが推奨されます。
これにより、親Componentだけに状態管理が集約します。 親Componentは、子Componentに「状態」と「状態を変更するための関数」を渡します。 子以下のComponentは、それらを受け取り利用するだけです。
ViewTree = ComponentTree(State)
親Componentに状態管理が集約されることで、アプリケーションの状態を把握することが容易になります。 それと同時に、子Componentはほとんどロジックを持たず、状態を受け取りViewを構築するStateless Functional Componentsで定義可能なただの関数になります。 これによりテストも容易になります。 加えて、親のComponentは状態管理、子のComponentは見た目に関してと、役割を明確に分けることができます。
しかしながら、このパターンの場合、親Componentに処理が集中するため、規模が大きくなると管理が難しくなります。 その解決方法として、Facebookが発表したFluxというアーキテクチャがあります。
Fluxでは、状態管理の流れを役割ごとに細かく分割し、React Componentから切り離します。
Fluxの影響を受けているライブラリーとして、Reduxがあります。 Reduxは、状態管理のためのライブラリーです。
Reduxでは、アプリケーションの状態をシリアライズ可能な1つのオブジェクトにまとめます。 状態の更新は、Actionを発行して、Reducerと呼ばれるActionを元に新しい状態を作成する関数によって行われます。
Reduxでは、React ComponentをContainer ComponentとPresentational Componentの2つの役割に分けます。 それぞれの役割は以下の通りです。
Reduxでは、データはシリアライズ可能なオブジェクトとして単一のStoreに保持されるため、各処理は入力を受け取り入力に応じた結果を出力するただの関数として実装できます。
- アクションの発行 –
action = ActionCreator([event]) - 状態の更新 –
newState = Reducer(state, action) - 状態の取得 –
props = Selector(state)
これにより、各部分もPresentational Componentのように簡単にテストができます。
状態と処理を切り離し、ActionとStateをシリアライズ可能なオブジェクトにすることは、デバッグや状態の再現を容易にします。 それを利用したRedux DevTools Extensionでは、Actionを記録して任意のActionの流れを再現するだけでなく、指定したActionの流れをJSONとしてインポート・エクスポートできます。 これにより、処理の流れを複数人で共有したり、エラーがあった場合にサーバーに送信することで、状況の再現が可能になります。
Reduxを使う場合、副作用や非同期処理の扱いを、Middlewareのレイヤーで吸収します。 Middlewareは発行されたActionを改変したり別のActionを発行するなど、アプリケーションの処理に大きく影響を与える部分です。 副作用や非同期処理を扱うためのMiddlewareは、すでにたくさん公開されています。 したがって、アプリケーションの要件に適した、副作用や非同期処理の方法を選択できます。
FluxアーキテクチャやReduxは、必要と感じるまで利用する必要はありません。 ですが、Reduxを使わない場合でも、React ComponentをContainer ComponentとPresentational Componentに分けて考えることは重要です。
エコシステム
Reactでは、すでに多くのエコシステムが存在します。 React Componentのテストを簡単に書くための仕組みを提供してくれるEnzymeや、Component単位でデザインを確認しながら開発できるReact StorybookなどOSSからたくさんのライブラリーやツールが公開されています。
また、React ComponentやJSXのためのESLintのプラグインも存在します。
コーディングのスタイルだけでなく、廃止予定のAPIの使用や、ベストプラクティスに沿っているかなど数多くの観点からのチェックが可能です。 ESLintとともに利用をオススメします。
また、a11yに対するチェックを行うeslint-plugin-jsx-a11yというプラグインもあります。
将来
Reactは、Facebookが開発しているライブラリーです。 Facebookの内部ではすでに25,000以上のReact Componentがあるそうで、ReactはFacebookの中でも重要な位置付けのライブラリーとなっています。 そのためFacebookが必要としている機能に対する優先度は当然高くなりますが、今後も継続してメンテナンスされることが期待できます。
加えて、OSSとしての役割も重要視されています。
例えば、SVGやcustom elementsの対応のようなFacebookでは必要とされていない機能も必要であればReact本体でサポートされます。
また、ReactのコアチームのミーティングノートをGitHubで公開したり、コマンド1つでReactを使ったアプリケーションの雛型を作成するcreate-react-appもその1つです。
前述した通り、Facebookだけでなく多くの企業がReactをプロダクションで使用しています。 そのため、Reactは非互換な変更に対してはとても慎重です。
何かAPIを廃止する場合は、「次のメジャーバージョンで警告の出力と移行プランの提供」、「その次のメジャーバージョンで廃止」という流れになります。 したがって、メジャーバージョンアップで突然動作しなくなることがないように配慮されています。
現在進行中の大きな開発として、「ReactFiber」があります。 ReactFiberは、ComponentとRendererがやりとりする、Reactのコアとなるアルゴリズムを全面的に書き直すものです。
現在は、更新処理からReactElementの差分計算・DOMへの反映までが同期的に行われています。 これは、規模の大きなComponentツリーや60FPSが求められるような場面で問題となることがあります。
ReactFiberでは、更新処理をFiberというタスクの単位で非同期に処理できるようになります。
例えば、アニメーションの更新処理など遅延が許されないものは、requestAnimationFrameを使い可能な限りすぐに反映し、APIのレスポンスや画面に表示されてない部分は、requestIdleCallbackを使い多少の遅延を許容するといった具合です。
ReactFiberは、現在ベースとなる部分が実装中であり、すぐに現在のアルゴリズムと置き換えられるものではありません。 しかしながら、今後注目すべき動向の1つです。
まとめ
ReactはComponentを使いViewを構築するためのライブラリーです。 そのためReactが担う範囲は広くありません。しかし、Reactを使うことでクライアントでの状態管理の難しさを排除し、シンプルに考えることができます。
また、Facebookのサイト自体もそうであるように、ReactはSingle Page Applicationではないアプリケーションに対しても、部分的に導入していくことが可能です。 その際に必要となる、設計としては綺麗とは言えないようなAPIも提供しています。
Reactを取り巻くエコシステムは大きくなっており、それらの組み合わせを紹介する記事も多く存在します。
しかしながら、Reactをこれから初めてみようと思っている場合は、最低限の構成で小さく始めることをオススメします。その後、必要だと思った段階で、エコシステムが提供するライブラリーの導入を順番に検討してください。
そうすることで、各ライブラリーが、「何を」問題と考えて「どのように」解決しようとしているのかを理解できるようになります。その結果、多くの情報やライブラリーに振り回されることなく、エコシステムと付き合っていけるようになると考えています。
上記の基調講演は動画でも公開中です
当日の資料と動画は下記で公開されていますので、こちらも参照してください。