Scaffolding Redux boilerplate with code generators

react javascript productivity redux

Image credit: Redux.js.org

In the previous post we saw how easy it is to get up and running with JavaScript code generators on the example of React components. In this post we'll build on that knowledge and dive deeper into generating code with a more advanced example - scaffolding Redux boilerplate. 

When I first started working with Redux I was impressed with its capabilities and how nicely it can abstract some complex component logic into actions. However, I was also surprised by how much boilerplate it requires to get properly setup in complex applications.

First you need to declare action types, then import them into action creators and define action creators themselves. Of course action creators are optional, but they make the code cleaner. Finally the action types have to be imported into reducer, which also requires setting up. The number of steps increases when you throw Redux middleware into the mix. This is particularly relevant in case you use Redux to handle API calls. In such case you often want to show loading indicator when data is being fetched, and then either display the data after it's loaded or show an error message when something goes wrong. I'd end up using three action types just for one API call - ACTION_BEGIN, ACTION_SUCCESS and ACTION_ERROR, or some variation of them. 

Let's speed up this particular case of setting up Redux actions for data fetching by generating boilerplate code with a generator. This generator will have two options - create new action from scratch or modify existing by adding a new one. The final code is available on Github.

We'll continue building on the example from the previous post and add a separate prompt for the Redux actions. First let's move the templates and config for the React component generator into their own separate folders and add the folders for Redux actions. 

After these changes we have file structure as follows.

mysite/
    src/
        scripts/
            generator/
                config/
                    react.js 
                    redux.js  
                templates/ 
                    react/ 
                    redux/
                config.js
                index.js
                listComponents.js
  

Separating configs for both generators will make it easier to navigate and update the code. We'll still keep all the prompts in the same file, however that can also be separated if needed. 

We'll start by adding more prompts to our main config.js.

description: "Generate new React component or Redux action",
  prompts: [
     {
      type: "list",
      name: "select",
      choices: () => [
        { name: "React Component", value: "react_component" },
        { name: "Redux Action", value: "redux_action" }
      ]
    },

  // React component prompts
  // ...
  {
      type: "list",
      name: "create_or_modify",
      message: "Do you want to create a new action or modify an existing one?",
      when: answer => answer.select === "redux_action",
      choices: () => [
        {
          name: "Create (will create new actions file)",
          value: "create"
        },
        {
          name: "Modify (will add the action to an existing one) ",
          value: "modify"
        }
      ]
    },
    {
      type: "list",
      name: "action",
      message: "Select action folder",
      when: ({ select, create_or_modify }) => {
        return select === "redux_action" && create_or_modify === "modify";
      },
      choices: listComponents("actions")
    },
    {
      type: "input",
      name: "action_prefix",
      message: "Action prefix (e.g. 'user'):",
      when: ({ select, create_or_modify }) =>
        select === "redux_action" && create_or_modify === "create",
      validate: value => {
        if (!value) {
          return "A name is required";
        }
        return true;
      }
    },
    {
      type: "input",
      name: "action_name",
      message: "Action name:",
      when: answer => answer.select === "redux_action",
      validate: value => {
        if (!value) {
          return "A name is required";
        }
        return true;
      }
    },
    {
      type: "confirm",
      name: "reducer_confirm",
      message: "Do you want to import actions into reducer?",
      when: ({ select }) => select === "redux_action"
    },
    {
      type: "list",
      name: "reducer_name",
      choices: listComponents("reducers"),
      when: ({ select, create_or_modify, reducer_confirm }) => {
        return (
          select === "redux_action" &&
          create_or_modify === "modify" &&
          reducer_confirm
        );
      },
      message: "Select reducer"
    },
  ],

At the topmost level we ask the user if they want to scaffold React component or Redux action. After this we'll have to add when: answer => answer.select === "redux_action" to all the prompt objects related to Redux actions and a similar one, checking for the answer with react_component, to React prompts. After that we follow a familiar path - checking if the user wants to create a new action from scratch or modify an existing one. If the choice is to create a new action we'll need to get a prefix for it (for ex. if you're scaffolding user actions, you provide user prefix and the generator will create userActions, userReducer, etc.). In case the choice is to modify existing action the user is asked to select which file to add the actions to. It should be mentioned that the following generator assumes you structure your Redux setup as follows, although it can be easily adjusted to any folder structure. 

mysite/
    src/
        actions/
            actionTypes.js
            testActions.js   
        reducers/
            initialState.js
            rootReducer.js
            testReducer.js

Also note that listComponents was modified to accept type parameter, so it's able to list files of different types.

const fs = require("fs");

module.exports = (type = "components") => {
  const names = fs.readdirSync("src/" + type);
  return names.map(i => i.replace(".js", ""));
};

After going through the prompts, it's time to get to the core of the generators, which is it's actions. We add them to redux.js file inside the config folder.

exports.reduxConfig = data => {
  const dirPath = `${__dirname}/../../..`;
  const reduxTemplates = `${__dirname}/../templates/redux`;
  let actions = [
    {
      type: "append",
      path: `${dirPath}/actions/actionTypes.js`,
      templateFile: `${reduxTemplates}/actionTypes.js.hbs`
    }
  ];
  let actionPath = `${dirPath}/actions/{{camelCase action_prefix}}Actions.js`;

  if (data.create_or_modify === "create") {
    actions = [
      ...actions,
      {
        type: "add",
        path: actionPath,
        templateFile: `${reduxTemplates}/create/actions.js.hbs`
      }
    ];

    // Create reducer
    if (data.reducer_confirm) {
      actions = [
        ...actions,
        {
          type: "add",
          path: `${dirPath}/reducers/{{camelCase action_prefix}}Reducer.js`,
          templateFile: `${reduxTemplates}/create/reducer.js.hbs`
        },
        // Add new reducer to the root reducer
        {
          type: "modify",
          path: `${dirPath}/reducers/rootReducer.js`,
          pattern: /\/\/plopImport/,
          templateFile: `${reduxTemplates}/create/rootReducer.js.hbs`
        },
        {
          type: "modify",
          path: `${dirPath}/reducers/rootReducer.js`,
          pattern: /\/\/plopReducer/,
          template: ",{{action_prefix}}\n//plopReducer"
        }
      ];
    }
  }
  if (data.create_or_modify === "modify") {
    actionPath = `${dirPath}/actions/{{camelCase action}}.js`;
    let reducerPath = `${dirPath}/reducers/{{reducer_name}}.js`;
    const actionType = "append";
    actions = [
      ...actions,
      {
        type: actionType,
        path: actionPath,
        pattern: /import {/,
        templateFile: `${reduxTemplates}/modify/actionImports.js.hbs`
      },
      {
        type: actionType,
        path: actionPath,
        templateFile: `${reduxTemplates}/modify/actions.js.hbs`
      }
    ];

    if (data.reducer_confirm) {
      actions = [
        ...actions,
        {
          type: actionType,
          path: reducerPath,
          pattern: /import {/,
          templateFile: `${reduxTemplates}/modify/actionImports.js.hbs`
        },
        {
          type: "modify",
          path: reducerPath,
          pattern: /\/\/plopImport/,
          templateFile: `${reduxTemplates}/modify/reducer.js.hbs`
        }
      ];
    }
  }

  return actions;
};

That's quite a bit of code, however in essence it boils down to 3 main pieces: actions for creating a new Redux action, actions for modifying it, and common actions for both cases. The common action here is to declare action types, the template for which looks like this:

// actionTypes.js.hbs

export const {{constantCase action_name}}_BEGIN = "{{constantCase action_name}}_BEGIN";
export const {{constantCase action_name}}_SUCCESS = "{{constantCase action_name}}_SUCCESS";
export const {{constantCase action_name}}_ERROR = "{{constantCase action_name}}_ERROR";

That's a lot of manual typing automated already! However, this is only the beginning. When creating or updating actions, we can scaffold action creators in a similar manner with this template:

// actions.js.hbs

import {
    {{constantCase action_name}}_BEGIN,
    {{constantCase action_name}}_SUCCESS,
    {{constantCase action_name}}_ERROR
} from './actionTypes';

export const {{camelCase action_name}}Begin = payload => ({
    type: {{constantCase action_name}}_BEGIN,
    payload
});

export const {{camelCase action_name}}Success = payload => ({
    type: {{constantCase action_name}}_SUCCESS,
    payload
});

export const {{camelCase action_name}}Error = payload => ({
    type: {{constantCase action_name}}_ERROR,
    payload
});

The reducer can be scaffolded like so:

// reducer.js.hbs

import {
    {{constantCase action_name}}_BEGIN,
    {{constantCase action_name}}_SUCCESS,
    {{constantCase action_name}}_ERROR
} from "../actions/actionTypes";

import initialState from "./initialState";

export default function(state = initialState.{{camelCase action_name}}, action) {
    switch (action.type) {
        case {{constantCase action_name}}_BEGIN:
        case {{constantCase action_name}}_SUCCESS:
        case {{constantCase action_name}}_ERROR:
            return state;
        //plopImport
    }
}

The rest of the templates can be examined in the Github repository

We're using a new action type - modify, which in contrast to append, replaces the text in the file located at path. In our case we use modify action to add generated code at a particular point in the template. To specify at which point the code should be inserted we provide a special //plopImport comment (it can be named anything) and then reference it in the pattern property of the action object. Since plop will replace this comment with the template it received, we need to remember to add the comment into the template, in the same place we'd like new code to be added. Another option could be to create own action to have more granular control over code generation. 

The final touch is to add the newly created Redux generator actions and combine them with existing React generator in the main config.js file.

// config.js

const { reactConfig } = require("./config/react");
const { reduxConfig } = require("./config/redux");


module.exports = {

  // Prompts

  actions: data => {
    return data.select === "react_component"
      ? reactConfig(data)
      : reduxConfig(data);
  }
}

Now the newly created generator is ready for a test drive. Note that before using it, you need to create actions and reducer folders, the latter one containing rootReducer.js.

And with that we have a handy generator that will abstract away a lot of manual work. This example can be extended further, for example to scaffold middleware actions, be it redux-thunk, redux-saga or anything else. 

Got any questions/comments or other kinds of feedback about this post? Let me know on Twitter.