この記事はReact Advent Calendar 2019の10日目担当@Sinack_jpです。
はじめに
タイトルが全てを物語っています。
reducerの種類がそこそこあるReact + Reduxなアプリケーションを開発するにあたって面倒なことをせずにreducerをモジュール化して、ハッピーなCode Splittingライフを送りましょうという記事です。
Code Splittingってそもそもなんぞ
Reactなどで作られたSPAは、バンドルされたJavaScriptファイルを読み込むことでアプリケーションとして動作することは皆さんご存知の通りと思います。
でも、アプリケーションの規模が大きくなってくると、バンドルされるファイルサイズもどんどん大きくなってきて、それに伴って起動時間がどんどん遅くなっていくわけです。
そこで、バンドルされるファイルの分割し、必要な部分を必要な時に動的に読み込むことでパフォーマンスを改善しましょうぞ。
というのがCode Splitting。
ReduxのCode splittingについて
で、本題に入ります。
実際にReact/Reduxなアプリケーションを開発する際には、複数のreducerを組み合わせてアプリケーションを開発することがほとんどだと思います。
そういった場合、combineReducers()
でreducerを束ねたrootReducerのようなものをcreateStore()
に渡して、みたいなことをするのが一般的かと思うのですが、それだけだとすべてのreducerが常にロードされている状態なので、reducerの種類が増えてくると、このときはこのreducerだけ読み込めてればいいのになあ、という状況が生まれる場合があります。
(もちろんすべてのreducerが常に読み込まれていないと機能しないような場合は読み込まないとだめです)
Reduxでは、Code Splittingの方法がドキュメントに記載されていて、そちらも読んではみたものの、なんかとにかく面倒くさそうなイメージと、redux-thunkやredux-sagaなどのミドルウェアが絡むとより一層わからん感が増してきて、うげっとなってしまっていました。
そんな感じでテンションが下がっていたところ、この記事の本題でもあるRedux Dynamic Modulesがページの下のほうにこそっと紹介されていたので使ってみたところ、なんかわかりやすいぞ。という気持ちになったので記事にしています。
Redux Dynamic Modulesについて
やっと本題です。
実はmicrosoftが作っているライブラリなので、ドキュメントがゴイスーちゃんとしています。
Redux Dynamic Modules
ドキュメントページ
Redux Dynamic Modulesをざっくり説明すると、storeとredux-thunk
, redux-saga
などのミドルウェアをグループとしてまとめたり、そのグループを好きなタイミングで読み込んだり外したりすることができるライブラリです。
非同期処理のミドルウェアはredux-thunk,redux-sagaに対応していて、今回の記事では扱いませんがこちらも簡単に組み込むことができます。
また、Reduxを使ったアプリケーション開発を行うときにはRedux Devtools Extensionがないと生きていけないのですが
Redux Dynamic Modulesを使うと、勝手にRedux Devtools Extensionも適用してくれます。地味に楽です。
サンプルを用意しました
今回のサンプルでは
- カウンター
- メッセージボード
という2つの機能があるアプリケーションを作りました。
カウンター内ではメッセージボードのreducer,storeは全く使う必要がなく、
逆も同様に、メッセージボード内ではカウンターのreducer,storeは全く必要としていません。
カウンターとメッセージボードは、react-router
で表示するコンポーネントを切り替えられるような作りになっていて
カウンターが表示されているときは、カウンターで必要なreducer,storeのみ、
メッセージボードが表示されている時は、メッセージボードで必要なreducer,storeのみを動的に付け外ししています。
あとせっかくなのでreact-routerはconnected-react-router
を使ってRedux storeで管理できるようにしています。
ふんわりした説明
/src/Counter
と/src/MessagesList
というディレクトリに、
コンポーネントやらactionやらreducerやらをまとめて入れてあります。
module.js
以外は至って普通のReact/Reduxな登場人物です。
module.js
の中はこんな感じになってます。
import couterReducer from "./reducer";
const counterModule = () => {
return {
id: "counter",
reducerMap: {
counter: couterReducer,
// initialActions: [hogeAction()],
// finalActions: [hugaAction()],
},
};
};
export default counterModule;
counterModule
が返すオブジェクトはカウンターで使うreducerをグループ化(といっても今回reducerは1つですが)idをつけ(ここではcounter)モジュールにしたものです。
reducerMap
には、このモジュールで使うreducerを複数指定することができ、このモジュールが読み込まれると、
ここで指定したreducerがすべて読み込まれる、という感じになります。
今回は使わないのでコメントにしてありますが、initialActions
やfinalActions
というキーを指定することで
このモジュールが追加されたとき、削除されたときに発火するアクションを指定することができます。便利・・・
んで次はコンポーネントファイルであるindex.jsを見てみます。
import React from "react";
import { DynamicModuleLoader } from "redux-dynamic-modules";
import { useCounter } from "./useCounter";
import counterModule from "./module";
const Counter = () => {
const { counterStore, increment, decrement } = useCounter();
return (
<div>
<div>カウンター:{counterStore}</div>
<button onClick={() => increment()}>+1</button>
<button onClick={() => decrement()}>-1</button>
</div>
);
};
export default () => (
<DynamicModuleLoader modules={[counterModule()]}>
<Counter />
</DynamicModuleLoader>
);
CounterコンポーネントをDynamicModuleLoader
コンポーネントでラップし、modules
propsにさきほどのオブジェクトを返す関数を渡しているのがポイントです。
こうしてやることで、DynamicModuleLoader
でラップされたコンポーネントがレンダーされるとき、指定したモジュールが動的にロードされます。
また、modules={[counterModule()]
となっていますが、ここではモジュールオブジェクトを配列で複数設定することも可能です。
ちなみにここでexportしたものは/src/routes.js
で読み込んでいます。
import React from "react";
import { Route, Switch } from "react-router-dom";
import Counter from "./Counter";
import MessagesList from "./MessagesList";
import Menu from "./Menu";
const routes = (
<>
<Menu />
<Switch>
<Route exact path="/" component={Counter} />
<Route path="/messages" component={MessagesList} />
</Switch>
</>
);
export default routes;
次に、おなじみProvider
にstoreを設定してあげるところを見てみましょう。
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { DynamicModuleLoader } from "redux-dynamic-modules";
import { Provider } from "react-redux";
import { history, configureStore } from "./store";
import * as serviceWorker from "./serviceWorker";
const store = configureStore();
ReactDOM.render(
<Provider store={store}>
<DynamicModuleLoader>
<App history={history} />
</DynamicModuleLoader>
</Provider>,
document.getElementById("root")
);
serviceWorker.unregister();
ここは普通にReduxを使うときとあまり変わらないです。
余談ですが、App
コンポーネントは、react-router
をRedux storeで管理するためのConnectedRouter
を返していて、
ConnectedRouter
は、さきほどのroutesをラップしています。
これだけだとconfigureStore()
が何をしているのかわからないので./store.js
を見てみましょう。
import { createStore } from "redux-dynamic-modules";
// import { getSagaExtension } from "redux-dynamic-modules-saga";
import { routerMiddleware } from "connected-react-router";
import { createBrowserHistory } from "history";
import { applyMiddleware, compose } from "redux";
import { routerModule } from "./modules/router/routerModule";
export const history = createBrowserHistory();
export const configureStore = (preloadedState = {}) => {
return createStore(
{
initialState: preloadedState,
enhancers: [compose(applyMiddleware(routerMiddleware(history)))],
// extensions: [getSagaExtension()],
},
routerModule()
);
};
export default configureStore;
普段は、redux
のcreateStore()
を使うところ、redux-dynamic-modules
のcreateStore()
を使っているのがポイントです。
コード内ではコメントアウトしてありますが、extensions
というキーでredux-sagaやredux-thunkの設定も行えます。
こうして無事にstoreをProvider
に設定することができました。
起動してみる
実際にアプリケーションを起動して確認してみると、下の画像のようにカウンターとメッセージボードで
それぞれ必要なstoreだけが読み込まれているのが分かると思います。
リンクをクリックすると、コンポーネントのレンダー時に、storeが切り替わるので試してみてください。
さいごに
Redux Dynamic Modulesは今回紹介した以外にも、モジュールの依存関係を設定するための機能などもあったりして、
とても便利に使えるライブラリです。スターが600ちょいしかついてないのが不思議でしょうがないです。
めちゃくちゃ便利なのに・・・
本当はconnected-react-routerやredux-saga、モジュールの依存やなんやを絡めていろいろ説明したかったのですが、
時間がなかったよすまん・・・
それはまたの機会にでもできたらなと思いまっす。
かなりダッシュかつざっくりしすぎた説明でReduxをあまり触らない方は意味わからん内容になってしまったかもと思いつつ
いつかどっかで紹介してやろうと思っていたRedux Dynamic Modulesを紹介できてよかったです。
本当にドキュメントわかりやすいので、この記事で興味をもっていただけたら是非使ってみてください!
それでは!