10
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ReactAdvent Calendar 2019

Day 10

Redux Dynamic Modulesで楽してReduxのCode Splittingしようぞ!

Last updated at Posted at 2019-12-10

この記事は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の中はこんな感じになってます。

/src/Counter/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がすべて読み込まれる、という感じになります。
今回は使わないのでコメントにしてありますが、initialActionsfinalActionsというキーを指定することで
このモジュールが追加されたとき、削除されたときに発火するアクションを指定することができます。便利・・・

んで次はコンポーネントファイルであるindex.jsを見てみます。

/src/Counter/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コンポーネントでラップし、modulespropsにさきほどのオブジェクトを返す関数を渡しているのがポイントです。
こうしてやることで、DynamicModuleLoaderでラップされたコンポーネントがレンダーされるとき、指定したモジュールが動的にロードされます。

また、modules={[counterModule()]となっていますが、ここではモジュールオブジェクトを配列で複数設定することも可能です。

ちなみにここでexportしたものは/src/routes.jsで読み込んでいます。

/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を設定してあげるところを見てみましょう。

/src/index.js
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を見てみましょう。

/src/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;

普段は、reduxcreateStore()を使うところ、redux-dynamic-modulescreateStore()を使っているのがポイントです。
コード内ではコメントアウトしてありますが、extensionsというキーでredux-sagaやredux-thunkの設定も行えます。

こうして無事にstoreをProviderに設定することができました。

起動してみる

実際にアプリケーションを起動して確認してみると、下の画像のようにカウンターとメッセージボードで
それぞれ必要なstoreだけが読み込まれているのが分かると思います。
リンクをクリックすると、コンポーネントのレンダー時に、storeが切り替わるので試してみてください。

adcal_gif.gif

さいごに

Redux Dynamic Modulesは今回紹介した以外にも、モジュールの依存関係を設定するための機能などもあったりして、
とても便利に使えるライブラリです。スターが600ちょいしかついてないのが不思議でしょうがないです。
めちゃくちゃ便利なのに・・・

本当はconnected-react-routerやredux-saga、モジュールの依存やなんやを絡めていろいろ説明したかったのですが、
時間がなかったよすまん・・・
それはまたの機会にでもできたらなと思いまっす。

かなりダッシュかつざっくりしすぎた説明でReduxをあまり触らない方は意味わからん内容になってしまったかもと思いつつ
いつかどっかで紹介してやろうと思っていたRedux Dynamic Modulesを紹介できてよかったです。

本当にドキュメントわかりやすいので、この記事で興味をもっていただけたら是非使ってみてください!
それでは!

10
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?