Optimize Redux Development: Simplify Boilerplate Creation with Code Generators and Plop.js

Updated on · 6 min read
Optimize Redux Development: Simplify Boilerplate Creation with Code Generators and Plop.js

In an earlier post we saw how easy it is to get up and running with JavaScript code generators, built with the Plop.js package, using React components as an example. In this post, we'll build upon that knowledge and delve deeper into code generation through a more advanced example - scaffolding Redux boilerplate.

Redux is a predictable state management library for JavaScript applications, often used with React, which helps developers maintain and manage application state more effectively through a unidirectional data flow. It offers great flexibility and can effectively abstract complex component logic into actions. However, many developers working with Redux eventually seek more efficient methods for managing Redux boilerplate. While packages like Redux Toolkit significantly improve this situation, there's still room for abstracting complex actions further.

In this article, we will investigate how to expedite your Redux development by automating the scaffolding of boilerplate code using Plop.js code generators. We will showcase how these generators can help minimize boilerplate and encapsulate repetitive tasks within the code generators' logic. By harnessing the power of this versatile tool, you'll achieve easier boilerplate management, effortlessly scaffold Redux code, and ultimately create a more enjoyable development experience. So, let's dive in and discover how to supercharge your Redux development workflow with Plop.js Redux code generators.

Getting started

Although tools like Redux-toolkit can help minimize boilerplate code, this tutorial aims to demonstrate the true potential of JavaScript code generators. We will enhance our React code generator by incorporating the ability to scaffold Redux actions for data fetching. The generator will provide two options: creating a new action from scratch or expanding an existing one. The final code is available on GitHub.

We'll build upon the example from the previous post and introduce a separate prompt for the Redux actions. First, let's relocate the templates and config for the React component generator into their distinct folders and create additional folders for Redux actions.

Following these modifications, our file structure appears as follows.

shell
react-generator/ src/ scripts/ generator/ config/ react.js redux.js templates/ react/ redux/ config.js index.js listComponents.js
shell
react-generator/ 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.

Adding prompts for the Redux code generator

We'll continue in the vein of the previous tutorial and start by adding more prompts to our main config.js.

javascript
// src/scripts/generator/config.js module.exports = { 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", }, ], };
javascript
// src/scripts/generator/config.js module.exports = { 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 prompt the user to choose between scaffolding a React component or a Redux action. Following this, we'll need to add when: answer => answer.select === "redux_action" to all the prompt objects related to Redux actions, and a similar condition, checking for the answer with react_component, to React prompts. Afterward, we proceed along a familiar path - determining 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 obtain a prefix for it (e.g., if you're scaffolding user actions, you provide the user prefix, and the generator will create userActions, userReducer, etc.). In the case of modifying an existing action, the user is prompted to select the file to which the actions should be added. It's worth noting that the following generator assumes you structure your Redux setup as illustrated below, although it can be easily adapted to any folder structure.

shell
react-generator/ src/ actions/ actionTypes.js testActions.js reducers/ initialState.js rootReducer.js testReducer.js
shell
react-generator/ src/ actions/ actionTypes.js testActions.js reducers/ initialState.js rootReducer.js testReducer.js

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

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

Adding actions for the Redux code generator

After reviewing the prompts, we can now focus on the heart of the generators - their actions. To do this, we add the actions to the redux.js file located within the config folder.

javascript
// src/scripts/generator/config/redux.js 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.push({ type: "add", path: actionPath, templateFile: `${reduxTemplates}/create/actions.js.hbs`, }); // Create reducer if (data.reducer_confirm) { actions.push( { 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.push( { 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.push( { 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; };
javascript
// src/scripts/generator/config/redux.js 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.push({ type: "add", path: actionPath, templateFile: `${reduxTemplates}/create/actions.js.hbs`, }); // Create reducer if (data.reducer_confirm) { actions.push( { 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.push( { 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.push( { 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; };

The code can be broken down into several parts:

  1. Initial setup: First we set up some paths and variables for later use. We define the base directory (dirPath) and the location of the Redux templates (reduxTemplates). Then we initialize an actions array, prefilling it with the action for adding a new Redux action type to the actionTypes.js file, as it will be common for all the subsequent paths.

  2. Create new Redux action: If the input data indicates that a new Redux action should be created (data.create_or_modify === "create"), the function adds an action object for generating a new action file. If the user also wants to create a reducer, it adds action objects for creating the reducer file and modifying the root reducer.

  3. Modify existing Redux action: If the input data indicates that an existing Redux action should be modified (data.create_or_modify === "modify"), we add action objects for appending to the existing action file. If a reducer is also being modified, we add action objects for appending to the reducer file.

  4. Return actions: Finally, we return the actions array, which is a collection of action objects that can be used by Plop to auto-generate the Redux code.

We are using a new action type called modify, which, unlike append, replaces text within the file found at the specified path. In our case, we use the modify action to insert generated code at a specific location in the template. To indicate the insertion point, we provide a unique //plopImport comment (which can be named differently) and reference it in the pattern property of the action object. Since Plop will replace this comment with the provided template, it is essential to include the comment in the template, exactly where we want the new code to be inserted. Alternatively, one could create a custom action for more granular control over code generation.

Adding Handlebars templates

First, we add a Handlebars template for declaring action types, as this will be common for all the generator's actions.

handlebars
// src/scripts/generator/templates/redux/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";
handlebars
// src/scripts/generator/templates/redux/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 similarly scaffold action creators with this template:

handlebars
// src/scripts/generator/templates/redux/create/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 });
handlebars
// src/scripts/generator/templates/redux/create/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:

handlebars
// src/scripts/generator/templates/redux/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 } }
handlebars
// src/scripts/generator/templates/redux/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 since they follow the same logic.

Final touches

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

javascript
// src/scripts/generator/config.js const listComponents = require("./listComponents"); const { reactConfig } = require("./config/react"); const { reduxConfig } = require("./config/redux"); module.exports = { description: "Generate new React component or Redux action", prompts: [ ... ], actions: (data) => { return data.select === "react_component" ? reactConfig(data) : reduxConfig(data); }, };
javascript
// src/scripts/generator/config.js const listComponents = require("./listComponents"); const { reactConfig } = require("./config/react"); const { reduxConfig } = require("./config/redux"); module.exports = { description: "Generate new React component or Redux action", 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.

Live demo

Conclusion

In conclusion, Plop.js code generators provide an excellent way to automate and simplify boilerplate creation in your Redux development workflow. By employing this powerful tool, you can eliminate repetitive tasks, enhance your productivity, and focus on the core logic of your applications. This tutorial walked you through a practical example of creating a Redux code generator with Plop.js, covering the configuration, prompts, actions, and Handlebars templates needed for a streamlined development process.

With a well-structured code generator in place, you'll be able to effortlessly scaffold Redux code and keep your projects organized, clean, and efficient. This will improve the developer experience, and your team will appreciate the time saved and the increased code consistency.

It took a moderate amount of effort to build a handy Redux code 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 middleware, Redux-Saga actions, or anything else.

References and resources