Dave Ceddia氏によるRedux vs. The React Context APIを本人の許可を得て意訳しました。
誤りやより良い表現などがあればご指摘頂けると助かります。
原文: https://daveceddia.com/context-api-vs-redux/
React 16.3は新Context API(旧Context APIはほとんど知られておらず、また非推奨であったことから、生まれ変わったと言っても良いでしょう)を追加しました。
Context APIはReactの第一級オブジェクトとして公式のものとなったのです。
React 16.3 が登場するや否や、Context APIがもたらすReduxの終焉についての記事がWeb上を賑わせました。しかしReduxは言うでしょう、「私が死ぬだって?馬鹿馬鹿しい」と。
この記事では、新Context APIの機能、Reduxとの類似点、Reduxの代わりにContextが使えそうな場面、そしてContextがあらゆる場面でReduxを置き換えるわけではないことについて説明します。
Contextの概要が知りたいだけならば、こちらまでスキップしても構いません。
単純なReactの例
前提条件として、Reactの基本(propsとstate)については熟知している必要がありますが、もし不安があれば5日間の無料React基本コースで学びましょう。
多くの人がRedux導入を検討する事例をベースに、まずは単純にReactのみの実装から始め、続いてReduxを導入し、最後はContextを試してみましょう。
このアプリケーションではユーザー情報が2箇所で表示されます。ナビゲーションバー内の右上部とメインコンテンツに隣接するサイドバー内です。
(Twitterのように見えるかも知れませんが偶然ではありません!React力を高める最善の方法はコピーワーク - 実在するアプリケーションの複製を作るです。)
コンポーネントの構造はこのようになります:
純粋なReact(通常のpropsだけ)では、ユーザー情報をツリー構造の上部で所有し、必要なコンポーネントに渡す必要があります。この場合では、 App
がユーザー情報を保持します。
続いて、 App
はユーザー情報を必要とする Nav
と Body
に渡します。それらは順番に、 UserAvatar
(万歳!)と Sidebar
に渡され、最終的には Sidebar
は UserStats
までバケツリレーします。
コードがどのようになるか見てみましょう(読みやすさのために全てを1つのファイルにまとめてしまっていますが、本来は良い感じの標準的な構造のように適宜分割した方が良いでしょう)。
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const UserAvatar = ({ user, size }) => (
<img
className={`user-avatar ${size || ""}`}
alt="user avatar"
src={user.avatar}
/>
);
const UserStats = ({ user }) => (
<div className="user-stats">
<div>
<UserAvatar user={user} />
{user.name}
</div>
<div className="stats">
<div>{user.followers} Followers</div>
<div>Following {user.following}</div>
</div>
</div>
);
const Nav = ({ user }) => (
<div className="nav">
<UserAvatar user={user} size="small" />
</div>
);
const Content = () => <div className="content">main content here</div>;
const Sidebar = ({ user }) => (
<div className="sidebar">
<UserStats user={user} />
</div>
);
const Body = ({ user }) => (
<div className="body">
<Sidebar user={user} />
<Content user={user} />
</div>
);
class App extends React.Component {
state = {
user: {
avatar:
"https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b",
name: "Dave",
followers: 1234,
following: 123
}
};
render() {
const { user } = this.state;
return (
<div className="app">
<Nav user={user} />
<Body user={user} />
</div>
);
}
}
ReactDOM.render(<App />, document.querySelector("#root"));
ここでは、 App
は user
オブジェクトを得るためにstateを初期化しますが、実際のアプリケーションでは、おそらくこのデータをサーバから取得してレンダリングのためにstateに保持するでしょう。
propドリルという面ではひどいものではなく、ちゃんと動作します。「Propドリル」はいかなる手段であれ非推奨ではなく、Reactが動作する完全に有効なパターンとして主に用いられます。しかし、深いドリルは多少冗長な記述になり、多くのprops(1つだけの時とは打って変わって)を渡す時にはさらに面倒になってきます。
この「propドリル」には大きなデメリットがあります。それは疎結合にすべきコンポーネント間に密結合を作り出してしまう点です。上記の例では、 Nav
は自身が user
を必要としないにも関わらず、user
を受け取って UserAvatar
に渡しています。
密結合したコンポーネント(子要素にpropsを渡さなければならないような)は再利用が難しくなります。新しい場所に置く時は常に新しい親とセットにしなければならないからです。
改善する方法を見ていきましょう。
ContextやReduxを導入する前に...
アプリケーション構造を統合して children
propを利用することができれば、深いpropドリル、Context、Reduxのいずれにも頼らずにクリーンなコードにできる可能性があります。
children
propはこの例で言う Nav
、 SideBar
、 そして Body
のような一般的なプレースホルダにする必要があるコンポーネントのための素晴らしい解決策になります。また、 children
と名付けられたpropだけでなく、あらゆるpropにJSX要素を渡すことも可能です。つまり、コンポーネントを拡張するための複数の「スロット」が必要な場合にはこのことを思い出してください。
Reactバージョンの事例です。こちらでは Nav
、 Body
、そして Sidebar
が children
を受け取り、そのままレンダリングしています。このようにコンポーネントの利用者はコンポーネントが必要とする特定のデータを渡す心配をする必要はありません。ユーザーは既にスコープ内にあるデータを使って単純に必要なものをそのままレンダリングします。この事例にはpropを使って children
を渡す方法も含まれます。
(Dan Abramovのこの提案に感謝します!)
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const UserAvatar = ({ user, size }) => (
<img
className={`user-avatar ${size || ""}`}
alt="user avatar"
src={user.avatar}
/>
);
const UserStats = ({ user }) => (
<div className="user-stats">
<div>
<UserAvatar user={user} />
{user.name}
</div>
<div className="stats">
<div>{user.followers} Followers</div>
<div>Following {user.following}</div>
</div>
</div>
);
// childrenを受け取ってレンダリングする
const Nav = ({ children }) => (
<div className="nav">
{children}
</div>
);
const Content = () => (
<div className="content">main content here</div>
);
const Sidebar = ({ children }) => (
<div className="sidebar">
{children}
</div>
);
// Bodyはsidebarとcontentを必要とするがこのようにも書ける
// これらのpropsには「何でも」渡せる
const Body = ({ sidebar, content }) => (
<div className="body">
<Sidebar>{sidebar}</Sidebar>
{content}
</div>
);
class App extends React.Component {
state = {
user: {
avatar:
"https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b",
name: "Dave",
followers: 1234,
following: 123
}
};
render() {
const { user } = this.state;
return (
<div className="app">
<Nav>
<UserAvatar user={user} size="small" />
</Nav>
<Body
sidebar={<UserStats user={user} />}
content={<Content />}
/>
</div>
);
}
}
ReactDOM.render(<App />, document.querySelector("#root"));
CodeSandboxの実装例はこちらです。
アプリケーションが複雑になるにつれ(この例よりも!)、 children
パターンを適用する方法が難しくなってくるでしょう。そこで、propドリルをReduxと置き換える方法を見ていきましょう。
Reduxの例
Contextがどう機能するのかを詳細に見ていけるようにReduxの例も簡単に触れておきましょう。Reduxについて不安がある方はReduxの紹介を読むと良いでしょう。(もしくはビデオを見てください)。
上記のReactアプリケーションをReduxを使ってリファクタリングした例を示します。 user
情報はRedux storeに移行するため、react-reduxの connect
関数を使って直接 user
propを必要に応じてコンポーネントに注入できるようになります。
これは疎結合という点で大きな利点となります。 Nav
、 Body
、 そして Sidebar
を見ると、もはや user
propを受け取ったり渡したりしていないことが分かるでしょう。もうpropにまつわる面倒なことを押し付け合う必要はありません。不要な密結合に別れを告げましょう。
ここでのreducerはあまり仕事をせず、かなりシンプルです。Reduxにおけるreducerの働きやイミュータブルなコードの書き方についての情報は他にもあります。
import React from "react";
import ReactDOM from "react-dom";
// createStore、connectそしてProviderが必要です:
import { createStore } from "redux";
import { connect, Provider } from "react-redux";
// 空の初期stateでreducerを作成します
const initialState = {};
function reducer(state = initialState, action) {
switch (action.type) {
// SET_USER actionに反応して直ちにstateを更新します
case "SET_USER":
return {
...state,
user: action.user
};
default:
return state;
}
}
// reducerを渡してstoreを作成します
const store = createStore(reducer);
// userをセットするためにactionをディスパッチします
// (初期stateは空なので)
store.dispatch({
type: "SET_USER",
user: {
avatar: "https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b",
name: "Dave",
followers: 1234,
following: 123
}
});
// このmapStateToProps関数はstate(user)から
// 1つのキーを抽出して `user` propsに渡します
const mapStateToProps = state => ({
user: state.user
});
// UserAvatarをconnect()して `user` を直接受け取ります
// 親コンポーネントから受け取る必要はありません
// 2つの変数に分割しても構いません
// const UserAvatarAtom = ({ user, size }) => ( ... )
// const UserAvatar = connect(mapStateToProps)(UserAvatarAtom);
const UserAvatar = connect(mapStateToProps)(({ user, size }) => (
<img
className={`user-avatar ${size || ""}`}
alt="user avatar"
src={user.avatar}
/>
));
// UserStatsをconnect()して `user` を直接受け取ります
// 親コンポーネントから受け取る必要はありません
// (どちらも同じmapStateToProps関数を使います)
const UserStats = connect(mapStateToProps)(({ user }) => (
<div className="user-stats">
<div>
<UserAvatar />
{user.name}
</div>
<div className="stats">
<div>{user.followers} Followers</div>
<div>Following {user.following}</div>
</div>
</div>
));
// Navは `user` について知る必要はありません
const Nav = () => (
<div className="nav">
<UserAvatar size="small" />
</div>
);
const Content = () => (
<div className="content">main content here</div>
);
// Sidebarは `user` について知る必要はありません
const Sidebar = () => (
<div className="sidebar">
<UserStats />
</div>
);
// Bodyは `user` について知る必要はありません
const Body = () => (
<div className="body">
<Sidebar />
<Content />
</div>
);
// Appはstateを保持しないため、ステートレスファンクションとなります
const App = () => (
<div className="app">
<Nav />
<Body />
</div>
);
// アプリケーション全体をProviderでラップし
// connect()によってstoreにアクセスできるようになります
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.querySelector("#root")
);
Reduxがこの魔法をどう実現しているのか疑問に思うでしょう。良い観点です。Reactがサポートしていない複数階層でのprops受け渡しをReduxはどのようにして行っているのでしょうか?
ReduxはReactのcontext機能を使っているというのがその答えです。新ContextAPIではなく、(まだ)旧Context APIです。Reactドキュメントにある通り、ライブラリを書いたり、何をしているのか理解している場合に限って使用しています。
Contextは全てのコンポーネントの背後にある電動式バスのようなもので、電力(データ)を受け取るにはプラグインするだけで済みますが、(React-)Reduxの connect
関数がまさにそれを担っています。
Reduxのこの機能は氷山の一角です。あらゆる場所にデータを渡せることは、Reduxの最もわかりやすい機能でしかありません。すぐに使えるその他の利点を紹介していきましょう。
connect
はピュアである
connect
は自動的にconnectされたコンポーネントを「ピュア」に保ちますが、それはprops、つまりRedux stateの変更をスライスしたものが変更された場合のみ再レンダリングされるということです。これにより不要な再レンダリングを抑制し、アプリケーションを高速に保ちます。PureComponent
を拡張したクラスもしくは、 shouldComponentUpdate
の独自実装のような自作メソッドです。
Reduxの簡単デバッグ
actionsやreducersを書く儀式は、素晴らしいデバッグ力によって安定します。
Redux DevTools拡張によって、アプリケーションが実行する各アクションのログを自動的に取得できます。任意のタイミングでそれを開くことで、発火したアクションの種類、ペイロードの中身、そしてアクション前後のstateを確認できます。
Redux DevToolsがもたらすもうひとつの素晴らしい機能は、タイムトラベルデバッグです。過去のアクションをクリックすると、その時点にジャンプして、基本的にそれ自身を含む全てのアクションを再実行します。各アクションのイミュータブルなstate更新によってそれが可能となりますが、記録されたstate更新一覧を取得して、副作用なしでそれを再生することができます。
また、本番環境のRedux DevTools上で常時稼働するLogRocketのようなツールもあります。バグレポートを取得できましたか?良いですね。LogRocketのユーザーセッションでは、彼らが何をしたか、もしくはどのアクションが発火したのかを正確に見ることができます。これらは全てReduxのアクションの流れを利用することで動作しています。
ミドルウェアによるReduxのカスタマイズ
Reduxはミドルウェアをサポートしていますが、これは「アクションがdispatchされるたびに実行される関数」という魅力的な概念です。自前のミドルウェアを書くことはそう難しくないうえ、強力な機能を実装することも可能です。
例えば...
- アクション名が
FETCH_
で始まるたびにAPIリクエストを開始したい場合は?ミドルウェアでそれができます。 - 分析ソフトウェアにイベントを記録する場所を集約したい場合は?ミドルウェアが良い選択です。
- 特定のアクションが特定回数実行されるのを防ぎたい場合は?ミドルウェアによってアプリケーション全体に透過的に実行可能です。
- JWTを持つアクションをインターセプトしてlocalStorageに自動的に保存したい場合は?そう、ミドルウェアですね。
Reduxミドルウェアの記述例はこちらです。
React Context APIの使い方
しかし、Reduxの魅力的な機能全てが必要とは限りません。簡単デバッグ、カスタマイズ、また自動パフォーマンス改善などではなく、簡単にデータを渡すことだけに関心があるかも知れません。アプリケーションが小さかったり、ただ動作するものが必要なだけだったりする場合は、魅力的なものについては後回しにしても良いでしょう。
Reactの新Context APIの出番です。どのように動作するのか見てみましょう。
読むことよりも見ることが好みな方のため、Eggheadで簡単なContext APIのレッスンを公開しました。(3:43)
Context APIには3つの重要なポイントがあります。
- contextを生成する
React.createContext
関数 - コンポーネントツリー内で「電動式バス」を確立する
Provider
(createContext
によって返される) - データを抽出するために「電動式バス」を利用する
Consumer
(こちらもcreateContext
によって返される)
Provider
はReact-Reduxの Provider
と良く似ています。 value
propを受け取りますが、何でも必要なもの(Redux storeでさえも...しかしそんな馬鹿なことをする人はいませんね)を渡せます。通常は、データもしくはデータ上で実行したいアクションを含むオブジェクトになるでしょう。
Consumer
はReact-Reduxの connect
関数に少々似ており、データを利用したり、それを使うコンポーネントが利用できるようにしたりします。
主要な部分はこのようになります。
// 最初に新しいcontextを作成します
// これは2つのプロパティを持つオブジェクトです: { Provider, Consumer }
// キャメルケースではなくアッパーケースで命名されていることに注目してください
// 後ほどコンポーネントとして利用するのでここが重要な点です
// コンポーネント名は大文字で始まる必要があります
const UserContext = React.createContext();
// Consumerプロパティを使ってcontextにデータを投入するコンポーネント
// Consumerはrender propsパターンを使用します
const UserAvatar = ({ size }) => (
<UserContext.Consumer>
{user => (
<img
className={`user-avatar ${size || ""}`}
alt="user avatar"
src={user.avatar}
/>
)}
</UserContext.Consumer>
);
// Consumerがcontextから取得するため
// 'user' propは不要です
const UserStats = () => (
<UserContext.Consumer>
{user => (
<div className="user-stats">
<div>
<UserAvatar user={user} />
{user.name}
</div>
<div className="stats">
<div>{user.followers} Followers</div>
<div>Following {user.following}</div>
</div>
</div>
)}
</UserContext.Consumer>
);
// ... 他の全てのコンポーネントはここにあります ...
// ... (`user` を知る必要のないコンポーネント群です)
// 最後に、アプリケーションの中でProviderを使って
// contextを子に渡します
class App extends React.Component {
state = {
user: {
avatar:
"https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b",
name: "Dave",
followers: 1234,
following: 123
}
};
render() {
return (
<div className="app">
<UserContext.Provider value={this.state.user}>
<Nav />
<Body />
</UserContext.Provider>
</div>
);
}
}
CodeSandboxの完全なコードはこちらです。
これがどう動くかを見てみましょう。
3つの部分があることを思い出してください。contextそのもの( React.createContext
で生成される)と、それと対話する2つのコンポーネント( Provider
と Consumer
)です。
ProviderとConsumerはペア
ProviderとConsumerは密結合かつ不可分であり、お互いに会話する方法しか知りません。2つの別のcontextを生成した場合、「Context1」と「Context2」と呼び、Context1のProviderとConsumerはContext2のProviderとConsumerと会話することはできません。
Contextはステートレス
contextが自身のstateを持たないことに注目してください。データのための通り道に過ぎません。valueを Provider
に渡し、そのままvalueはそれを見つける方法を知っている Consumer
(ConsumerはProviderと同じcontextに束縛されます)に渡されます。
contextを生成した時、「デフォルト値」はこのように渡せます。
const Ctx = React.createContext(yourDefaultValue);
このデフォルト値は Consumer
が Provider
を持たないツリー内にある時に受け取る値です。その値がなければ、 undefined
になるだけですが、飽くまでもデフォルト値であって初期値ではないことに注意してください。contextは何も保持せず、単に渡したデータを配布するだけです。
ConsumerはRender Propsパターン
Reduxの connect
関数は高階コンポーネント(別名HoC)です。他のコンポーネントをラップしてpropsを渡します。
contextの Consumer
は対象的に子コンポーネントを関数として受け取ります。レンダリングにその関数を呼び出し、親階層のどこかにある Provider
から得られた値(もしくはcontextのデフォルト値かデフォルトが未設定であれば undefined
)を渡します。
Providerは1つの値を受け取る
1つの値だけを value
propとして受け取りますが、これはどんなデータ型でも構いません。実際には、複数の値を渡したい時には、全ての値を持つオブジェクトを生成して渡します。
これはContext APIの根幹となるものです。
Context APIは柔軟
contextを生成すると2つのコンポーネント(ProviderとConsumer)が提供されるため、必要に応じて自由に使用できます。いくつかのアイデアを示しましょう。
ConsumerをHoC化する
UserContext.Consumer
を必要な場所にばらまくのは好みではないですか?まあ、あなた次第です!もう大人ですし、やりたいようにしてください。
valueをpropとして受け取ることを好むのであれば、 Consumer
の小さなラッパーをこのように書くことができます。
function withUser(Component) {
return function ConnectedComponent(props) {
return (
<UserContext.Consumer>
{user => <Component {...props} user={user}/>}
</UserContext.Consumer>
);
}
}
そしてまた UserAvatar
をこの新しい withUser
関数を使うように書き直すことができます。
const UserAvatar = withUser(({ size, user }) => (
<img
className={`user-avatar ${size || ""}`}
alt="user avatar"
src={user.avatar}
/>
));
そして何と、contextはReduxの connect
のように動作することも可能ですが、自動純度は劣ります。
HoCを使ったCodeSandboxの例はこちらです。
Providerにstateを保持する
contextのProviderは通り道に過ぎないということを忘れないでください。一切のデータは保持されませんが、データを保持するためのラッパーを作ることを制限するものではありません。
上記の例では、 App
がデータを保持したままにしておき、新しく覚えるのはProviderとConsumerだけで済むようになっています。しかし独自の「store」のようなものが欲しくなるかも知れません。stateを保持してcontext経由で渡すコンポーネントを作ることもできます。
class UserStore extends React.Component {
state = {
user: {
avatar:
"https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b",
name: "Dave",
followers: 1234,
following: 123
}
};
render() {
return (
<UserContext.Provider value={this.state.user}>
{this.props.children}
</UserContext.Provider>
);
}
}
// ... 中間は省略 ...
const App = () => (
<div className="app">
<Nav />
<Body />
</div>
);
ReactDOM.render(
<UserStore>
<App />
</UserStore>,
document.querySelector("#root")
);
これでuserデータはコンポーネント自身に格納され、唯一の関心はuserデータのみとなります。素晴らしいですね。 App
は再びステートレスになり、よりクリーンになりましたね。
UserStoreを使ったCodeSandboxの例はこちらです。
Context経由でアクションを渡す
Provider
経由で渡されるオブジェクトは何でも必要なものを含めることができます。関数も含めることが可能です。それらは「アクション」と呼ばれるものです。
新しい例はこちらです。シンプルな部屋に背景色をトグルする電気スイッチがあります。
stateはstoreに保持され、電気をトグルする関数もまた保持されます。stateと関数のいずれもcontext経由で渡されます。
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
// プレーンな空のcontext
const RoomContext = React.createContext();
// Roomのstateを管理するのが唯一の仕事であるコンポーネント
class RoomStore extends React.Component {
state = {
isLit: false
};
toggleLight = () => {
this.setState(state => ({ isLit: !state.isLit }));
};
render() {
// stateとonToggleLightアクションを渡す
return (
<RoomContext.Provider
value={{
isLit: this.state.isLit,
onToggleLight: this.toggleLight
}}
>
{this.props.children}
</RoomContext.Provider>
);
}
}
// 電気のstateと電気をトグルする関数をRoomContextから受け取ります
const Room = () => (
<RoomContext.Consumer>
{({ isLit, onToggleLight }) => (
<div className={`room ${isLit ? "lit" : "dark"}`}>
The room is {isLit ? "lit" : "dark"}.
<br />
<button onClick={onToggleLight}>Flip</button>
</div>
)}
</RoomContext.Consumer>
);
const App = () => (
<div className="app">
<Room />
</div>
);
// RoomStore内のアプリケーション全体をラップします
// これは `App` 内と同様に動作します
ReactDOM.render(
<RoomStore>
<App />
</RoomStore>,
document.querySelector("#root")
);
CodeSandboxの完全な動作サンプルはこちらです。
ContextとReduxのどちらを使うべきか?
ここまで2つの手法を見てきましたが、どちらを使うべきでしょうか?アプリケーションをより良くし、書くのを楽しくするのであれば、それが意思決定を支配します。「答え」を知りたいと思うでしょうが、残念ながら「場合による」としか言えません。
アプリケーションの大きさや成長余地などによります。どれくらいの数の開発者が関わるでしょうか?個人開発もしくは大きなチームでしょうか?あなたやチームは関数型のコンセプト(Reduxが依存している不変性や純粋関数など)にどれだけ習熟していますか?
JavaScriptのエコシステムに蔓延る大きな誤解の1つは、競合の考え方です。あらゆる選択肢がゼロサムゲームであるという考えです。ライブラリAを使ったらその競合であるライブラリBを使ってはいけないという考え。新たなライブラリが登場した場合、何らかの点が優位性を持つという考えから、既存のライブラリに取って代わらなければならないという考え。全てにおいて白黒つける必要があり、最善かつ最新を選ばなければ、老兵と共に表舞台を去らねばならないという認識があります。
より良いアプローチは、ツールボックスのようなこの素晴らしい配列を見てみることです。通常のドライバーとインパクト式ドライバーのどちらを使うのかを選ぶようなものです。仕事の80%では、インパクト式ドライバーは通常のドライバーよりも速くネジを回します。しかし残りの20%では、通常のドライバーが実際に良い選択になります。作業場所が狭かったり、対象が壊れやすかったりするかも知れないからです。インパクト式ドライバーを手に入れても、すぐに通常のドライバーやインパクト式でないドリルを捨てたりはしません。インパクト式ドライバーはそれらを置き換えるものではなく、単に異なる選択肢を与えてくれるだけです。問題を解決する別の方法です。
ContextはReduxを「置き換える」ものではありません。ReactがAngularやjQueryを「置き換える」ものではないのと同様です。何と、私は何か手早く作りたい時には今でもjQueryを使いますし、サーバでレンダリングされたEJSのテンプレートをReactアプリケーションの代わりに使うことだってあります。Reactは手元のタスクをこなすのにはオーバースペックな時もあります。Reduxは大げさな時もあります。
Reduxを使うほどでもないと思えば、Contextを使えば良いのです。