redux-dynamic-module と

typescript-fsa と

re-ducks を

組み合わせて開発してみたお話

前提

大規模 SPA WebApp 開発

  • 利用ユーザに応じて表示モードを切り替える
    • CS向け / 修理向け / 製造向け など
    • 認証やユーザ検索など共通部品もあり

モジュール単位での開発をしたい!

よく見る redux ディレクトリ構成 (Rails-style)

悩み

→ 関心空間の単位でディレクトリ分離したい!

  • 関連するコードがあちこちに分散
  • 機能分割でファイル数を増やしづらい
  • 部品内での共通関数などを置く場所が作り辛い

re-ducks

redux 構成要素の

ディレクトリルール

&ネーミングルール

  • 関心空間の単位でディレクトリを分ける
  • redux の機能単位でファイル独立さる
    • reducers, actions ...
  • 公開するものと隠すものを分類する
    • 外部からの参照用の index を用意する
    • Selectors
      • モジュール状態参照
      • 状態参照関数が用意される
    • Operations
      • モジュール操作
      • ActionCreator が用意される

rails-style

re-ducks

ディレクトリ分離できた!

Code Splitting したい

悩み

他の表示モードでしか使わないものは読み込みたくない

 

けど、store とか reducer / middleware って

 lazy loading できるんだっけ?

redux-dynamic-modules

store / reducer / middleware などを

付け外しする機構

  • MIT License
  • Copyright (c) Microsoft Corporation.

 

  • 2018.10.30 に 1.0.0
  • 現在 4.0.1 (2019.6.15)

 

  • 動作中にモジュールの load / unload 可能
  • store の構造に工夫がある
import { IModule } from 'redux-dynamic-modules';

// dependencies

// application imports
import { Actions, Operations } from './operations';
import { Selectors } from './selectors';
import Reducer, { IState } from './reducers';

// ============================================================================
// Type definitions
//
export interface IModuleOwnState
{
  app: IState;
}

// ============================================================================
// Dynamic Module
//
function getModule(): IModule<IModuleOwnState> {
  return {
    id: "ApplicationModule",
    reducerMap: {
      app: Reducer,
    } as any,
    // Actions to fire when this module is added/removed
    initialActions: [Actions.initialize(null)],
    // finalActions: []
    // middlewares: [],
  };
}

// ============================================================================
// Exports
//
export {
  IState,
  Selectors,
  Operations,
};

export const Modules = [
  // My module
  getModule(),
];

module

 

reducer と middleware

情報を持つ object

 

  • id
  • reducerMap
  • middlewares
  • initialActions
  • finalActions

 

他 module への依存関係は配列で表現

import React, {lazy} from 'react';

const CSWidget      = lazy( () => import('./widgets/CS'      /* webpackChunkName: "CSWidget" */) );
const FactoryWidget = lazy( () => import('./widgets/Factory' /* webpackChunkName: "FactoryWidget" */) );
const RepairWidget  = lazy( () => import('./widgets/Repair'  /* webpackChunkName: "RepairWidget" */) );

const Component: React.FC<IProps> = (props) => {
  const { mode } = props;

  return (
  	<div>
    	{mode === 'CS'      && <CSWidget />}
        {mode === 'Factory' && <FactoryWidget />}
        {mode === 'Repair'  && <RepairWidget />}
    </div>
  );
}

module 読込例

コンポーネントのタブを書くだけで

store も reducer も middleware も

利用可能に!

createStore

createStore

  • redux-dynamic-modules が createStore を用意している
  • 既存の store との互換性は無い !!

 

必要なタイミングで

モジュール読み込み

できるようになった!

型、大切ですよね

悩み

dispatch すると型情報が伝播しない

 

reducer や middleware で値を扱うときに型情報を使うには?

typescript-fsa

Action と型を結びつける

  • MIT License
  • Copyright (c) 2016 Daniel Lytkin

 

  • 2019.9.14 に 3.0.0 になった

 

  • FSA(Flux Standard Action) 対応の Action を作成する
  • Action ハンドリングの際に型情報の取得ができる
    • typescript-fsa-reducers / typescript-fsa-redux-thunk etc.
import actionCreatorFactory from 'typescript-fsa';

const actionCreator = actionCreatorFactory('ApplicationModule');

export const Actions = {
  initialize : actionCreator('initialize'),
  
  fetch : actionCreator.sync<IParam, IUser[], IError> ('fetch'),
}

action creator

import { reducerWithInitialState } from "typescript-fsa-reducers";
import { Actions } from './actions';

export interface IState {
  users: IUser[];
}

const initialState = () : IState => {
  users: [],
}

export default reducerWithInitialState( initialState() )

.case( Actions.fetch.done, (state, {result}) => {
  return {...state, users: result}
})

reducer

  • Vanilla middleware 向けの typescript-fsa-* が無かった
    • 作ってみた

typescript-fsa-redux-middleware

import { middleware } from "typescript-fsa-redux-middleware";
import { Operations, Actions } from './operations';

export default middleware()

.case( Operations.fetchUsers, async ({dispatch, getState}, next, action) => {
  const params = action.payload;

  await dispatch( Actions.fetch.started(params) );
  try {
  	const users = await fetch('URL');
	dispatch( Actions.fetch.done({params, result: users}) );
  } catch(error) {
    dispatch( Actions.fetch.failed({params, error}) );
  }

  next(action);
})

middleware

redux loop で型情報が

行きわたるようになった

まとめ

大規模 react + redux アプリ作成

でモジュール化は必須

  • re-ducks で関心空間の分離
  • redux-dynamic-modules でモジュール化
  • typescript-fsa で型情報の伝播

関心空間の単位で

モジュール実装できる環境が整った!

typescript-fsa-redux-middleware

使ってみてね

今回紹介した技術を含めたサンプルを用意してあります

★ もお願いします!!!

おわり

redux-dynamic-module と typescript-fsa と re-ducks を組み合わせて開発してみたお話

By Yasuharu Seki

redux-dynamic-module と typescript-fsa と re-ducks を組み合わせて開発してみたお話

  • 1,416