Code Splitting de Reducers e Sagas usando Webpack2 e React Router 4

Code Splitting de Reducers e Sagas usando Webpack2 e React Router 4

Uma Single Page Application tem no tamanho de código que é preciso descarregar de cada vez um dos seus principais problemas.

Uma aplicação pode ter muitas funcionalidades para as quais temos de ter código implementado, mas este não é necessáriamente utilizado sempre por todos os utilizadores a todo o momento.

Uma das formas que permitem melhorar esta situação é a divisão de código em ficheiros mais pequenos importados apenas quando são necessários. Um potencial caso em que se poderá usar este "code-splitting" é na implementação de uma secção de Administrador numa aplicação.

Redux Store

Um dos passos necessários para implementar esta solução passa por termos a possibilidade de acrecentar reducers à store quando necessário. Para isso vamos adaptar a store.

  /**
   * store.js
   */
  import { createStore, applyMiddleware, compose } from 'redux';
  import { createReducer } from './rootReducer';

  export const configureStore = (sagaMiddleware) => {
    const store = createStore(createReducer(), compose(
      applyMiddleware(sagaMiddleware),
    ));
    store.sagaMiddleware = sagaMiddleware;
    store.asyncReducers = {};
    return store;
  };

  export const injectAsyncReducer = (store, name, asyncReducer) => {
    store.asyncReducers[name] = asyncReducer;
    store.replaceReducer(createReducer(store.asyncReducers));
  };

  export default configureStore;

Daqui os elementos que nos interessam para o caso que estamos a discutir são o store.sagaMiddleware = sagaMiddleware; que nos vai permitir aceder ao sagaMiddleware mais tarde e o injectAsyncReducer que nos vai permitir acrescentar reducers quando necessário.

O rootReducer que está a ser importado é o seguinte:

  /**
   * rootReducer.js
   */
  import initialReducer from './initialReducer';

  export const createReducer = (asyncReducers) => {
    return combineReducers({
      initialReducer,
      ...asyncReducers,
    });
  };

  export default rootReducer;

Esta construcção simplesmente expande a utilização normal do combineReducers e permite-nos acrescentar reducers sempre que a função é chamada.

Agora vamos iniciar a aplicação. Para começar vamos criar a store e a history e iniciar as sagas.

  /**
   * index.js
   */
  const sagaMiddleware = createSagaMiddleware();
  const store = configureStore(sagaMiddleware);
  sagaMiddleware.run(sagas);
  const history = createBrowserHistory();

  // ...

e a seguir renderizar os componentes

  const Index = () => (
    <Provider store={store}>
      <Router history={history}>
        <MainApp>
          <Switch>
            <Route exact path="/" component={Home} />
            {
              // `Routes` da aplicação
            }
            <Route path="/admin" component={() => <Administrador store={store} />} />
          </Switch>
        </MainApp>
      </Router>
    </Provider>
  );

  export default Index;

A alteração que fizemos aqui foi simplesmente acrescentar a store como uma propriedade a passar para o componente. Isto vai permitir-nos utilizar o injectAsyncReducer mais tarde.

Vamos agora definir o componente Administrador que irá carregar de forma asíncrona os sub-componentes, os reducers e as sagas sempre que for iniciado.

  import React, { Component } from 'react';
  import { shape, func } from 'prop-types';
  import { Route, withRouter } from 'react-router-dom';
  import { connect } from 'react-redux';
  import { injectAsyncReducer } from '../../store';
  import Loader from './Loader';
  import Toolbar from './Toolbar';

  export class Administrador extends Component {
    constructor(props) {
      super(props);
      this.state = {
        pathA: Loader,
        pathB: Loader,
      };

      this.setComponents = this.setComponents.bind(this);
    }

    async componentDidMount() {
      const { default: reducer } = await import('./reducer');
      injectAsyncReducer(this.props.store, 'reducer', reducer);

      const { default: adminSagas } = await import('./sagas');
      this.props.store.sagaMiddleware.run(adminSagas);

      const { default: pathA } = await import('./Encomendas/Encomendas');
      const { default: pathB } = await import('./Compras/Compras');

      this.setComponents(Dashboard, pathA, pathB);
    }

    setComponents(pathA, pathB) {
      this.setState({ pathA, pathB });
    }

    render() {
      return (
        <div>
          <Toolbar />
          <div>
            <Route path="/admin/pathA" component={this.state.pathA} />
            <Route path="/admin/pathB" component={this.state.pathB} />
          </div>
        </div>
      );
    }
  }

  export default withRouter(connect()(Administrador));

O componente Administrador quando é inicializado vai apresentar os vários urls com o Loader por defeito. Para conseguirmos carregar de forma asíncrona os ficheiros adicionais vamos definir a função componentDidMount com sendo async. Isto permite que dentro da função possamos usar o await: por exemplo const { default: adminReducer } = await import('./reducer');.

Quando este import é chamado o código vai parar até que exista um resultado. O async / await é uma forma mais simples de definir uma Promise, de utilização semelhante ao function*() / yield usada nas sagas. Para o podermos usar é preciso utilizar o plugin Babel "transform async generator functions".

O primeiro await import vai devolver um reducer que vamos acrescentar à store via a funcção injectAsyncReducer.

Algo semelhante é feito no import seguinte. Desta vez vamos importar a saga e fazê-la correr usando a função run do sagaMiddleware. Quer este quer o injectAsyncReducer pressupõe acesso directo à store, e foi por isso que acrescentar a este componente a nossa store como uma propriedade. Os dois imports seguintes vão carregar os componentes a ser usados em cada um dos paths. Ao actualizarmos o state vamos forçar o re-render que vai alterar o Loader pelo componente que carregamos.

Imagem de Drew Graham via unsplash.