DEV Community

Cover image for Speed up your React developer workflow with code generators
Alex K.
Alex K.

Posted on • Originally published at claritydev.net

Speed up your React developer workflow with code generators

The article was originally posted on my personal blog.

As React developers we often need to setup new components, hook them up with the existing infrastructure or scaffold an application. That's a lot of repeated manual work, which even though doesn't happen that often, can be quite tedious and frankly, boring. The good news is that it can be easily automated with code generators. These generators can be also shared with other developers, increasing code consistency inside a team.  

In this post we'll use plop package to setup generators that would create React component folders either from scratch or add a new component to already existing folder. The final code is available on Github.

Assuming you already have a React app setup (I personally prefer create-react-app to speed up the process), we'll start by installing plop.

 

    npm i -D plop
Enter fullscreen mode Exit fullscreen mode

-D here is a shortcut for --save-dev.  At the same time let's add generate script to our package.json.

    // package.json

    "generate": "./node_modules/.bin/plop --plopfile src/js/scripts/generator/index.js",
Enter fullscreen mode Exit fullscreen mode

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

The base structure is typical for an app made with create-react-app. Additionally, each component has a folder with the component files and index.js, from where all the components are exported.

    mysite/
        src/
            components/
                Component1/
                    Component1.js
                    index.js 
            App.js
            App.css
            index.js
            index.css
Enter fullscreen mode Exit fullscreen mode

Now we'll create scripts folder in the src directory, inside of which we'll add generator folder. Inside generator let's add index.js, where we'll setup the generator itself, named "component".

    // index.js

    const config = require("./actions");

    module.exports = function(plop) {
      plop.setGenerator("component", config);
    };
Enter fullscreen mode Exit fullscreen mode

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 documentation, the generator config object has 3 properties:

  • description - short description of what this generator does
  • prompt - questions to collect the input from the user
  • action - actions to perform, based on the input

Let's start by adding the description.

    // config.js

    /**
     * Generate React component for an app
     */

    module.exports = {
        description: "Generate a new React component"
    }
Enter fullscreen mode Exit fullscreen mode

Well, that was easy. Now let's define the prompts, which are basically the ways to get input from the user.

 

    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" }
          ]
        }
      ],
Enter fullscreen mode Exit fullscreen mode

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 for prompts, so in case you want to have a deeper look at the prompt types available, check their repository.

The way prompts work, is after the input from the user is collected, it is 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 user selects an option, it's 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 basically checks if the current prompt should be shown to the user. So in this case if the user selected add action, the next prompt will ask to specify a directory to which a component should be added. 

You'll notice that listComponents utility function is used here to get an array of component names in components directory.

    // listComponents.js

    const fs = require("fs");
    const path = require("path");

    module.exports = () => {
      return fs.readdirSync(path.join(__dirname, `../../components`));
    };
Enter fullscreen mode Exit fullscreen mode

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

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

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

 

    // config.js

    const componentsPath = "../../components";

    // ...

      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 = [
            ...actions,
            {
              type: "add",
              path: `${componentsPath}/{{properCase name}}/index.js`,
              templateFile: "./templates/index.js.hbs"
            }
          ];
        }

        if (data.action === "add") {
          actions = [
            ...actions,
            {
              type: "append",
              path: `${componentsPath}/{{dir}}/index.js`,
              templateFile: "./templates/index.js.hbs"
            }
          ];
        }

        return actions;
      }
    }
Enter fullscreen mode Exit fullscreen mode

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

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

First, we fill the array with default action, which will create a new component either in directory selected from dropdown or, in case it's a new component folder, in the folder with that name. Next there are two paths - when 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 handlebars syntax to define paths to our files. These strings have access to the data from prompt, for example by doing {{properCase name}} we're accessing the name of the component that user typed in the prompt. Combining this with ES6 string interpolation provides a powerful way to configure our paths.

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

    // index.js.hbs

    export {default as {{ properCase name }}, } from "./{{ properCase name }}";

    // 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 }};

    // 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 }};
Enter fullscreen mode Exit fullscreen mode

We use the format filename.js.hbs to show the target's file type. The templates are quite simple, they are basically stubs for respective files with the component's name missing. It's worth noting that plop's helper methods are also available in the templates, which is very handy for customizing output. 

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

Alt Text

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. 

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

Top comments (4)

Collapse
 
clarity89 profile image
Alex K.

Thanks for the comment. made me revisit the topic of tree shaking in JS. :)

I don't think we're "throwing out the idea of tree shaking out the window" here, since we don't import everything but only what's needed. E.g. import {ComponentName} from './ComponentFolder'.

Collapse
 
chrisachard profile image
Chris Achard

Plop looks interesting! I'll have to check it out.

One minor point: at one point you said "react-create-app", but I think you may have meant "create-react-app".

Thanks for the post!

Collapse
 
clarity89 profile image
Alex K.

Nice spot! I've edited the article, thanks for the hint :)

Collapse
 
aminmansuri profile image
hidden_dude

What I always wonder about code generators is if you can abstract it down to a more succinct language, then why not just program in that language and let it interpret at runtime? Why do I need to boilerplate at all?