Friday, 4 November 2016

Splitting React Redux reducers into multiple files

Using redux for state management of your application is a really great and light way of having a single source of truth for your application; The problem is that sometimes reducers just become too big to maintain. Luckily we have the ability to split these up into smaller, more dedicated reducers with ES6 import/export and Redux's combineReducers method. Let's have a look.

Building the store

Install redux and react-redux packages.
$ npm i -D redux react-redux
We need to create the store and create the method to configure the store. Let's have a look at the store configuration method first:
/app/store/configureStore/index.js
import { compose, createStore } from 'redux';

import reducer from 'reducer';

const configureStore = (params = {}) => {
  const { initialState } = params;
  const store = createStore(
    reducer,
    initialState,
    compose(
      window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(),
    ),
  );
};

export default configureStore;
/app/store/index.js
import configureStore from './configureStore';

export const store = configureStore();
Now you can add your store to the store provider provided by react-redux.
/app/root/index.jsx
import React from 'react';
import { Provider } from 'react-redux';

const App = () => (
  <Provider store="{store}">
    <h1>App content here</h1>
  </provider>
);

export default App;

State and file structure

For this example we will be working towards creating a state that looks something like the following:
{
  app: {
    account: {
      name: '',
      email: '',
    },
    products: {
      data: [
        { id: 1, name: '', favourite: false },
        { id: 2, name: '', favourite: false },
      ],
      settings: {
        layout: 'grid',
      }
    },
  },
}
The file structure will very closely resemble out state tree:
app
--account
----actions.js
----index.js
--products
----data
------index.js
------product
--------actions.js
--------index.js
----settings
------actions.js
------index.js

Creating the reducers

We will be creating a reducer for each 'node' in our state (each key in our state object). Let's start by creating the top level 'app' node in the main reducer file, which is being passed to the 'configureStore' method described above.
/app/reducers/index.js
import { combineReducers } from 'redux';

import app from './app';

const reducer = combineReducers({
  app,
});

export default reducer;
Now we will build the 'app' reducer which will add the 'account' and 'products' nodes. Because the 'app' node contains more sub-nodes ('account' and 'products') the app reducer should only be concerned with combining the reducers for each sub-node, this will look like follows:
/app/reducers/app.js
import { combineReducers } from 'redux';

import account from './account';
import products from './products';

const app = combineReducers({
  account,
  products,
});

export default app;
The next reducer - the one for the 'account' node - will contain some logic to perform actions. We will split it up into two files, the reducer, and the actions. Let's build the actions file first:
/app/reducers/account/actions.js
export const ACCOUNT_CHANGE_NAME = 'ACCOUNT_CHANGE_NAME';

export const accountChangeName = name => ({
  type: ACCOUNT_CHANGE_NAME,
  name
});
Now the reducer:
/app/reducers/account/index.js
import { ACCOUNT_CHANGE_NAME } from './actions';
export const defaultState = {
  name: '',
  email: '',
};

const account = (state = defaultState, action = {}) => {
  switch (action.type) {
    case ACCOUNT_CHANGE_NAME:
      return {
        ...state,
        name: action.name,
      };
    default:
      return state;
  };
};

export default account;
The next node - 'products' - is another reducer that combines to sub-reducers for it's two sub-nodes, 'data' and 'settings', so it looks almost identical to the 'app' node's reducer. The 'settings' node's reducer will look almost identical to the 'account' node's reducer. The 'data' node returns an array of 'product' nodes. Let's build a reducer for the 'data' node and for each 'product' sub-node:
/app/reducers/products/data/index.js
import productReducer from './product';

const defaultState = [];

const data = (state = defaultState, action = {}) => {
  return [ ...state.map(product => productReducer(product, action)) ];
};

export default data;
The product reducer would then need actions and a reducer, let's build the actions first:
/app/reducers/products/data/product/actions.js
export const PRODUCT_ADD_TO_FAVOURITES = 'PRODUCT_ADD_TO_FAVOURITES';

export const productAddToFavourites = id => ({
  type: PRODUCT_ADD_TO_FAVOURITES,
  id,
});
/app/reducers/products/data/product/index.js
import { PRODUCT_ADD_TO_FAVOURITES } from './actions';

export const defaultState = {
  id: 0,
  name: '',
  favourite: false,
};

const productReducer = (state = defaultState, action = {}) => {
  switch (action.type) {
    case PRODUCT_ADD_TO_FAVOURITES:
      return {
        ...state,
        favourite: state.id === action.id ? !state.favourite : state.favourite,
      };
  };
};

export default productReducer;
Now we have split our reducer into many smaller, more maintainable - and testable - chunks.

No comments:

Post a Comment