図は英語記事からの引用です。
JavaScript: Reduxが必要なとき/不要なとき(翻訳)
Reduxが登場するまで、複雑なタスクを組むときのステート管理は相当つらい作業でした。Reduxは、Fluxというアプリケーションデザインパターンにヒントを得て、JavaScriptアプリでステートを管理するために設計されました。ReduxはReactと併用されることが多いのですが、ReduxはjQueryやAngular、Vueといった別のフレームワークとも併用できます。
Reduxのサイズは非常に小さい(依存関係も含めてわずか2KB)にもかかわらず、アプリの各コンポーネントが自分のステートに直接アクセスできるようになります。このとき、子コンポーネントにprops
を送信する必要も、親コンポーネントがデータを受け取るのにコールバック関数を使う必要もありません。
本記事では、Reduxがいかに関数型プログラミングに深く根ざしているか、そしてアプリにReduxを導入するかどうかをうまく決定する方法について解説したいと思います。
「どうしてReduxが必要なの?」
ピカピカの新しいツールが登場するたびにいきなり飛びついてプロジェクトに導入したりしないのは今や常識です。結局のところ、あらゆるコンポーネントはステートを持たないのでしょうか?ステート管理ツールがなぜ今必要なのでしょうか?
誤解しないでいただきたいのですが、Reactはそれだけで十分素晴らしいフレームワークであり、他に何もなくても完全なアプリを書けます。しかしアプリが複雑になるに連れてコンポーネントの数も増加し、フレームワークだけでこれらを管理するのは非常に難しくなることがあります。
そこに登場するのが複雑なアプリを扱いやすくしてくれるReduxです。Reactを少し使ってみれば、Reactのデータフローが親コンポーネントから子コンポーネントにprops
を渡す作業であることがわかります。多数のコンポーネント間をステートやpropsを介してデータフローが行き交う巨大なアプリ内ではコミュニケーションエラーが発生しやすくなり、(信じていただきたいのですが)コードはいつしか読むのも改善するのも非常に困難になってしまいます。
理解のため、以下の図をご覧ください。
Reactでは(他のフレームワークでも同様ですが)、親子関係にないコンポーネント同士でのやりとりがスムーズにできません。Reactでは、それが必要な場合にはFluxのパターンに従ってグローバルなイベントシステムを構築せよとアドバイスしています。そしてここがReduxの出番なのです。
Reduxを使うと、アプリの全ステートを保持する「ストア」が使えるようになります。コンポーネントAでステートが変更されるとそのことがストアに伝わります。コンポーネントAのステート変更を監視する必要のあるコンポーネントBやCはストアにサブスクライブすることで、ステートの変更がストアから中継されます。
おわかりいただけましたでしょうか?思ったよりずっとよくできています。コンポーネント間のコミュニケーションを元のまま放置していれば、やがてエラーが生じやすくなり、とても読んでいられないコードベースになってしまいます。Reduxはこの流れを変えます。
コンポーネントAが自分のステート変更をストアに送信すると、コンポーネントBとCがステートの変更を必要としている場合に、BとCがストアからAの変更を取得できます。このようにして、データフローのロジックがシームレスになります。
Reduxの本来の目的以外にも、Reduxを使うメリットはたくさんあります。中でも私が重要だと思う部分をリストアップします。
- 1. 結果が予測可能
「信頼できる情報源」(ストア)が1つに限定されているので、アクションとアプリの他の部分の現在のステートを同期するときに問題がほとんど生じません。
- 2. メンテナンスしやすい
Reduxではコードの編成方法に関する厳密なガイドラインが定められています。これにより結果がさらに予測しやすくなり、コードのメンテナンスも容易になります。
- 3. テストしやすい
Reduxで書くコードは、テストしやすいコードを書くための黄金律に従って分離される純粋な関数になります。1つの作業だけを行う、独立した小さい関数を書くようにしましょう。
「Reduxって結局要らなくない?」
当然とお考えの方もいらっしゃるかと思いますが、念のため申し上げます。Reduxを無理して使う必要はありません。Reduxを使わない理由が今ひとつ見えにくいこともあるかもしれませんが、以下のいずれかに該当する場合は、おそらくReduxは完全に不要でしょう。
- 自分や友人(同僚でも構いませんが)が、コンポーネント間でステートの共有や加工を行う方法を既に定義済みの場合
- Reactなどのフレームワーク経験がまだ浅い場合
- アプリのほとんどの動作で今後もシンプルなUI変更などぐらいしか行う予定がなく、Reduxストアに含める必要もなければコンポーネントレベルで扱えるようにする必要もない場合
- サーバーサイドイベント(SSE)やWebSocketsを管理する必要がない場合
- ビューごとに1つのデータソースからフェッチする場合
Reduxの構成要素
初心者が戸惑わないよう、Reduxのライブラリは2KBに抑えられ、ツール自体が「アクション」「ストア」「レデューサー」の3つのパーツで構成されています。
https://stackoverflow.com/questions/45416237/axios-calls-in-actions-reduxより
アクション(action)
アクションは、関数を用いて生成されるシンプルなイベントであり、アプリのデータをストアに送信します。データの送信は、「フォームの送信」「API呼び出し」「基本ユーザー操作」などさまざまな方法で行われる可能性があります。Reduxのあらゆるアクションにはtype
プロパティがあり、アクションの種類(type)や、ストアに送信される情報の「ペイロード」を記述します。実際の最も基本的なアクションの例をご覧ください(Gist)。
// The action
{
type: ADD_USER,
payload: {
username: ‘Chris’,
email: ‘redux @frameworks.io’
}
}
// The function that creates the action
function signUpUser(data) {
return {
type: ADD_USER,
payload: data
}
}
Reduxには、アプリのどこからでもアクションを呼べるようにするためのdispatch()
メソッドが用意されています。このメソッドはアクションをReduxストアに送信してステートの変更を表明します(Gist)。
dispatch(signUpUser(data));
レデューサー(reducer)
Reduxではステートをアプリから(直接)変更できないようになっています。変更にはdispatch()
を使います。dispatch()
はステート変更の意図を表明するだけのメソッドであり、実際の変更はReduxが行います。
レデューサーは関数であり、ディスパッチされたアクションを介してアプリの現在のステートを受け取り、新しいステートを返します。以下のレデューサーが、現在のステートとアクションを引数として受け取り、次のステートを返す様子をご覧ください(Gist)。
function handleAuth(state, action) {
return _.assign({}, state, {
auth: action.payload
});
}
もっと複雑なアプリを作る場合は、ReduxのcombineReducers()
メソッドの利用が推奨されます。このメソッドはアプリ内のすべてのレデューサーを1つのレデューサーリストにまとめます。あらゆるレデューサーはここでアプリのステートのパーツを扱い、ステートのパラメータはレデューサーごとに異なります(Gist)。
const indexReducer = combineReducers({
signUp: signUp,
editProfile: editProfile,
makePayment: makePayment
});
ここでもうひとつ知っていただきたいのは、レデューサーは「純粋関数」で記述すべきという点です。純粋関数の特徴を以下にリストアップしました。
- 外部ネットワークや外部データベースへの呼び出しを行わない
- 戻り値は、パラメータの値だけに応じて変わる
- 引数はイミュータブルとみなされるべき(変更されるべきでないという意味)
ストア(store)
ストアはReduxの心臓部です。ストアは、アプリのあらゆるステートを保持する単一の信頼できる情報源であり、ステートへのアクセス/アクションのディスパッチ/リスナーの登録を行うメソッドを提供します。ディスパッチされたアクションは、レデューサを介して常に新しいステートをストアに保存します。Reduxストアの基本的な例をご覧ください(Gist)。
import { createStore } from‘ redux’;
let store = createStore(indexReducer);
let signUpInfo = {
username: ‘Chris’,
email: ‘redux @frameworks.io’
};
store.dispatch(signUpUser(signUpInfo));
関数型プログラミングとRedux
Reduxを使うことになったら、関数型プログラミングの動作について知っておくべきです。Reduxは関数型プログラミングの原理に基づいて作られているので、関数型プログラミングのコンセプトを理解しておくことでReduxがどのようにして操作を行っているかについて洞察を得ることができます。
関数型プログラミングの重要な点についてガイドラインをざっと追ってみましょう。
- 純粋で、再帰可能で、高階のクロージャや無名関数を利用できる
- map/filter/reduceなどのヘルパー関数を利用できる
- 関数同士を互いにチェインできる
- 関数は第一級オブジェクトとして扱える
- 関数を引数として渡せる
- 関数/再帰/配列を用いてフローを制御できる
- 関数のステートは変更されない(イミュータブル)
- コードの実行順序は重要ではない
関数型プログラミングでは、シンプルで小さく分離された関数を書きます。このパターンに沿うことでコードのメンテナンスやテストやデバッグがやりやすくなります。関数は小さく分離されているので再利用しやすくなり、必要に応じてどこにでもコピペできます。
関数型プログラミングは、必要なコード量が少なくて済む点が個人的に素晴らしいと思っています。関数型プログラミングを使うときは、上述の「純粋関数」「無名関数」「クロージャ」「高階関数」のコンセプトを理解しておくことが重要です。
まとめ
Reduxはアプリのステート管理のための素晴らしいライブラリであり、Reduxが多くの開発者を魅了していることは確かです。その他にどんなことを知っておく必要があるでしょうか?
ReduxはUberやTwitterなどの企業で大規模に用いられているだけではなく、WordPressなどのプロジェクトでも実装に成功しています。Reduxはどんな既存アプリにも使える万能ライブラリではないという論争が起きていることも確かですし、そしてそのとおりです。
おそらく、シンプルなアクションだけを用いるアプリや、サーバーサイドレンダリングの不要なアプリではReduxは不要でしょう。Reduxはアクションをコンポーネントレベルで扱いますそうしたアプリのアクションはコンポーネントレベルで扱えます。
いずれにしろReduxの素晴らしさに変わりはありません。特にReactをお使いの方なら、ぜひReactReduxをチェックすべきでしょう。
お知らせ: 本番のReactアプリを隅々まで可視化しましょう
Reactアプリケーションは、ユーザーが遭遇した問題の再現が困難な場合にひときわつらくなります。Reduxのステートを監視・トラッキングしたい方、JavaScriptのエラーを自動的に検出表示したい方、低速なネットワークリクエストのトラッキングやコンポーネントロード時間をトラッキングしたい方は、ぜひLogRocketをお試しください。
LogRocketは、ちょうど録画や再生のように、Reactアプリで発生することを文字どおりすべて記録します。問題の原因を闇雲に推測するのではなく、問題が発生した瞬間にアプリケーションがどのような状態であったかを管理画面で確認できます。LogRocketはアプリのパフォーマンスを監視して、クライアントのCPU負荷やメモリ使用量などのメトリクスをレポートします。
LogRocketのReduxミドルウェアパッケージを使えば、ユーザーセッションを見やすく表示するレイヤが追加されます。LogRocketは、Reduxストアのあらゆるアクションとステートをログに出力します。
Reactアプリを現代的にデバッグしましょう。無料の監視機能をお試しください。
概要
原著者の許諾を得て翻訳・公開いたします。