Speed Up Your React Developer Workflow with Plop.js Code Generators

Updated on · 8 min read
Speed Up Your React Developer Workflow with Plop.js Code Generators

As React developers, we often find ourselves setting up new components, connecting them with the existing infrastructure, or scaffolding applications. That's a lot of repetitive manual work, which even though doesn't happen that often, can be quite tedious and frankly, boring. Fortunately, there is a solution that can help us to automate this process and save time: JavaScript code generators.

One popular tool for generating JavaScript code is Plop.js. It enables the easy automation of code scaffolding as well as the creation of any other type of text files. Even though it's referred to by its creators as a "micro-generator framework", it's very powerful. With this tool, we can easily create generators that automatically generate React code for us. These generators can create component files, test files, and more, all based on templates that we define. By using a code generator built with Plop, we can reduce the amount of manual work required to set up new components and increase the consistency of our codebase.

In this post, we will explore how to use the Plop package to auto-generate React components. While you could use something like VS Code snippets, they are editor specific and are not easily shared with multiple developers. Utilizing a code generator for React, on the other hand, not only saves time and effort but also ensures consistency across your project, as the generated code adheres to the pre-defined templates and patterns. Moreover, these generators can be shared with other developers in your team, promoting a uniform coding style and fostering collaboration.

In this tutorial, we will go through the process of setting up the Plop generator for React, demonstrating how to auto-generate React code effectively from a pre-defined set of templates. The final code for this tutorial is available on GitHub, allowing you to dive in and explore how the generator works, as well as adapt it for your projects.

This tutorial shows how to set up code generators for React components. The next article in the series explores generating Redux boilerplate using Plop-based JavaScript code generators: Optimize Redux Development: Simplify Boilerplate Creation with Code Generators and Plop.js.

Setting up Plop.js

To set up generators for the React code, we need to create a React project first. Let's do that using create-react-app to speed things up.

bash
npx create-react-app react-generator
bash
npx create-react-app react-generator

Next we'll install the Plop.js package as a dev dependency.

bash
npm i -D plop
bash
npm i -D plop

At the same time let's add the generate script to our package.json.

json
// package.json "scripts": { "generate": "./node_modules/.bin/plop --plopfile src/scripts/generator/index.js" },
json
// package.json "scripts": { "generate": "./node_modules/.bin/plop --plopfile src/scripts/generator/index.js" },

If you install Plop globally (with -g prefix), you can use the plop command instead of ./node_modules/.bin/plop.

The base structure follows the typical pattern for an app created using create-react-app. In addition, each component resides in a dedicated folder containing the component files and an index.js file, which exports all the components.

shell
react-generator/ src/ components/ Component1/ Component1.js index.js App.js App.css index.js index.css
shell
react-generator/ src/ components/ Component1/ Component1.js index.js App.js App.css index.js index.css

Now, we will create a scripts folder within the src directory and add a generator folder inside it. Within the generator folder, we will include an index.js file, where we will set up the generator itself, named "component."

javascript
// src/scripts/generator/index.js const config = require("./config"); module.exports = function (plop) { plop.setGenerator("component", config); };
javascript
// src/scripts/generator/index.js const config = require("./config"); module.exports = function (plop) { plop.setGenerator("component", config); };

We still need to add the config for the generator, which is the main part of our setup. For that, let's create config.js and start fleshing it out.

If we look at the Plop's documentation, the generator config object has 3 properties:

  • description - short description of what this generator does (optional)
  • prompts - questions for collecting input from the user
  • actions - actions to perform, based on the input

Let's start by adding the description.

javascript
// src/scripts/generator/config.js /** * Generate React component for an app */ module.exports = { description: "Generate a new React component", };
javascript
// src/scripts/generator/config.js /** * Generate React component for an app */ module.exports = { description: "Generate a new React component", };

Adding prompts for the React code generator

Well, that was easy. Now, let's set up the prompts, which are essentially the methods to collect input from the user.

javascript
module.exports = { description: "Generate a new React component", prompts: [ { type: "list", name: "action", message: "Select action", choices: () => [ { name: "Create component folder", value: "create", }, { name: "Add separate component", value: "add", }, ], }, { type: "list", name: "component", message: "Select component", when: (answer) => answer.action === "add", choices: listComponents, }, { type: "input", name: "name", message: "Component name:", validate: (value) => { if (!value) { return "Component name is required"; } return true; }, }, { type: "list", name: "type", message: "Select component type", default: "functional", choices: () => [ { name: "Functional component", value: "functional" }, { name: "Class-based Component", value: "class" }, ], }, ], };
javascript
module.exports = { description: "Generate a new React component", prompts: [ { type: "list", name: "action", message: "Select action", choices: () => [ { name: "Create component folder", value: "create", }, { name: "Add separate component", value: "add", }, ], }, { type: "list", name: "component", message: "Select component", when: (answer) => answer.action === "add", choices: listComponents, }, { type: "input", name: "name", message: "Component name:", validate: (value) => { if (!value) { return "Component name is required"; } return true; }, }, { type: "list", name: "type", message: "Select component type", default: "functional", choices: () => [ { name: "Functional component", value: "functional" }, { name: "Class-based Component", value: "class" }, ], }, ], };

The main properties of each object in the prompts array are type, name, and message. If the type of prompt is list, we need to provide a list of choices for it. Plop uses Inquirer.js, a collection of common interactive command line user interfaces, for prompts. Each question is of type InquirerQuestion, where you can see all the other properties available for it.

The way prompts work is that once the input from the user is collected, it becomes available as a property on the argument of the prompt's methods. For example, in the first prompt above, we provide an array of choices to select from. After the user selects an option, its value will be available on the action property of the data object, because we specified the name of the prompt as action. Then, in the next prompt object, we can access this value in the when method: when: answer => answer.action === "add".

The when property essentially checks if the current prompt should be shown to the user. In this case, if the user selected the add action, the next prompt will ask them to specify a directory to which a component should be added.

You'll notice that the listComponents utility function is used here to get an array of component names in the components directory. Let's implement it next.

javascript
// src/scripts/generator/listComponents.js const fs = require("fs"); const path = require("path"); module.exports = () => { return fs.readdirSync(path.join(__dirname, `../../components`)); };
javascript
// src/scripts/generator/listComponents.js const fs = require("fs"); const path = require("path"); module.exports = () => { return fs.readdirSync(path.join(__dirname, `../../components`)); };

Additionally, we use the validate function to ensure that the user has specified the component's name. In the last prompt, we ask to select the type of component to be created, providing the option of a functional component as the default one, since it will likely be used most often.

Adding actions for the React code generator

Now comes the most interesting aspect of the JavaScript code generator - its actions. Actions can be a list of commands to execute or a function that returns such a list. In this example, we'll use the functional form since we need to perform several checks and conditional returns.

Before that, let's add one constant at the top of the file, componentsPath, which will save us from the trouble of updating paths in multiple places, in case we decide to move the config elsewhere.

javascript
// src/scripts/generator/config.js const componentsPath = "../../components"; module.exports = { description: "Generate a new React component", prompts: [ ... ], actions: data => { const target = data.action === "create" ? "properCase name" : "dir"; let actions = [ { type: "add", path: `${componentsPath}/{{${target}}}/{{properCase name}}.js`, templateFile: "./templates/{{type}}.js.hbs" } ]; if (data.action === "create") { actions.push({ type: "add", path: `${componentsPath}/{{properCase name}}/index.js`, templateFile: "./templates/index.js.hbs" }); } if (data.action === "add") { actions.push({ type: "append", path: `${componentsPath}/{{dir}}/index.js`, templateFile: "./templates/index.js.hbs" }); } return actions; } }
javascript
// src/scripts/generator/config.js const componentsPath = "../../components"; module.exports = { description: "Generate a new React component", prompts: [ ... ], actions: data => { const target = data.action === "create" ? "properCase name" : "dir"; let actions = [ { type: "add", path: `${componentsPath}/{{${target}}}/{{properCase name}}.js`, templateFile: "./templates/{{type}}.js.hbs" } ]; if (data.action === "create") { actions.push({ type: "add", path: `${componentsPath}/{{properCase name}}/index.js`, templateFile: "./templates/index.js.hbs" }); } if (data.action === "add") { actions.push({ type: "append", path: `${componentsPath}/{{dir}}/index.js`, templateFile: "./templates/index.js.hbs" }); } return actions; } }

The actions method takes a data object as an argument. This object contains all the data collected by the prompts. The method needs to return an array of action objects, the most important properties of which are:

  • type - the kind of operation this action will perform. Here we have actions that will create a new file - add, or modify an existing file - append.
  • path - the location of the created or modified component.
  • templateFile - a path to the Handlebars template used to create or modify a file. Alternatively, a template property can be used, which is handy for short Handlebars templates that don't need to be in separate files.

First, we fill the array with the default action, which will create a new component either in the directory selected from the dropdown or, in case it's a new component folder, in the folder with that name. Next, there are two paths - when a new component folder is created, we add an index.js file to the folder; if it's a new component file, we'll modify index.js with the new export. Plop has a few handy built-in text transformers that we use here, namely properCase, which will ChangeTextToThis. Also, we can use the Handlebars syntax to define paths to our files. These strings have access to the data from the prompt, for example by doing {{properCase name}} we're accessing the name of the component that the user typed in the prompt. Combining this with ES6 string interpolation provides a powerful way to configure our paths.

Setting up Handlebars templates

Now let's look at the templates that are used to generate and modify the files.

handlebars
// src/scripts/generator/index.js.hbs export {default as {{properCase name}}, } from "./{{properCase name}}";
handlebars
// src/scripts/generator/index.js.hbs export {default as {{properCase name}}, } from "./{{properCase name}}";
handlebars
// src/scripts/generator/functional.js.hbs import React from 'react'; import PropTypes from 'prop-types'; /** * * {{properCase name}} * */ const {{properCase name}} = (props) => { return ( <div> {{properCase name}} </div> ); } {{properCase name}}.propTypes = {}; export default {{properCase name}};
handlebars
// src/scripts/generator/functional.js.hbs import React from 'react'; import PropTypes from 'prop-types'; /** * * {{properCase name}} * */ const {{properCase name}} = (props) => { return ( <div> {{properCase name}} </div> ); } {{properCase name}}.propTypes = {}; export default {{properCase name}};
handlebars
// src/scripts/generator/class.js.hbs import React, { Component } from 'react'; import PropTypes from 'prop-types'; /** * * {{properCase name}} * */ class {{properCase name}} extends Component { static propTypes = {} constructor(props) { super(props); this.state = {}; } render() { return ( <div> {{properCase name}} </div> ); } } export default {{properCase name}};
handlebars
// src/scripts/generator/class.js.hbs import React, { Component } from 'react'; import PropTypes from 'prop-types'; /** * * {{properCase name}} * */ class {{properCase name}} extends Component { static propTypes = {} constructor(props) { super(props); this.state = {}; } render() { return ( <div> {{properCase name}} </div> ); } } export default {{properCase name}};

We use the format filename.js.hbs to indicate the target's file type. The templates are relatively straightforward, essentially serving as stubs for the respective files with the component's name missing. While not many developers use React class-based components anymore, we keep the template for those components here for demonstration purposes and also because those types of components have more boilerplate than functional ones. It's worth noting that Plop's helper methods are also available in the templates, which is incredibly useful for customizing the output.

Now let's try our generator in action to verify that it works.

Demo of the generator in a terminal

Awesome! Generating new components is now just a command away. This is quite a simple example, however, it nicely demonstrates the power of code generators. It can be easily expanded and becomes even more useful for components with a lot of boilerplate. For example, if each component has some translations setup or a large list of imports or if you want to generate TypeScript React components with a lot of type-related boilerplate.

Conclusion

In conclusion, utilizing a Plop generator for React projects can greatly enhance the developer experience by streamlining the process of creating new components and scaffolding applications. By auto-generating React code using predefined templates, you can save time, ensure consistency across your codebase, and maintain a uniform coding style among team members.

Code generators like Plop.js offer an efficient way to create and customize components, test files, and more. React code generation not only reduces manual work but also promotes collaboration among team members, allowing them to focus on developing features and functionality rather than repetitive tasks.

In this tutorial, we explored the process of setting up a Plop generator for React and demonstrated how to auto-generate React code effectively using a set of pre-defined templates. With this knowledge, you can adapt and expand the generator to suit your projects and requirements, enabling you to focus more on developing features and functionality rather than on repetitive and tedious tasks. Give React code generation a try and discover the difference it can make in your development workflow. Happy coding!

References and resources