Austin Malerba Software Engineer at Target, BS in CS, immature amateur.

Automatically generate React components with Plop.js

7 min read 2077

Automatically Generate React Components With Plop.js

Editor’s note: This post was recently updated. It may still contain information that is out of date.

Do you often find yourself copying and pasting old components as templates for new components? I know I do.

Having worked with Angular before, I felt frustrated that there is no React equivalent to the Angular CLI’s generate API. For those who are unfamiliar with it, the Angular CLI offers utilities to generate components, routes, services, and pipes, all from the command line.

Then, I started thinking, why is there no React equivalent? It occurred to me that the React ecosystem is not as standardized as the Angular ecosystem.

A new React app doesn’t ship with much; directory structure and dependencies are largely left up to the developer. This can be nice, but the lack of uniformity among React applications makes tools such as React component generators a bit harder to tackle.

After some investigation, I stumbled upon a library called Plop.js. In this post, we’ll demonstrate Plop’s capabilities by building a React component generator.

What is Plop.js?

The Plop.js library can be used with React and lets you define your own parameterized code templates, which can inject into your source code to generate boilerplate code that you would otherwise have to write yourself.

The neat thing about Plop is that it provides a fairly high-level API while it’s simultaneously very customizable. And we’re not limited just to components; we can generate containers, services, Hooks, reducers, actions, utilities, docs, readme’s, and more.

Plop.js project structure

Before we can generate boilerplate code automatically, we must know the boilerplate we want to generate.

With the following structure used for React apps, let’s build our generators with this structure in mind.

We made a custom demo for .
No really. Click here to check it out.

Plop.js Project Structure In Code

A few things to note about this setup:

Building the CLI interface

Now that we know how we want to structure our code, we can focus on the CLI interface we want to build. For the purposes of this article, we will generate components, pages, Hooks, and services.

To keep it simple, the only argument we must provide for each is a name:

# Generate a single-use component
npm run generate page <name>
# Generate page called Home
npm run generate page Home

# Generate a reusable component
npm run generate component <name>
# Generate a component called DogCard
npm run generate component DogCard

# Generate a custom hook
npm run generate hook <name>
# Generate a hook called useAsync
npm run generate hook useAsync

# Generate a service
npm run generate service <name>
# Generate a service called petApi
npm run generate service petApi

Building the component generator

Configuring Plop.js

Let’s create a new React app and configure Plop.js:

# Create new react app
npx create-react-app react-starter-cli
# Change working directory to root of app
cd react-starter-cli
# Install plop as a dev-dependency
npm install --save-dev plop
# Make a directory for our templates
mkdir plop-templates
# Create a plopfile to hold our cli logic
touch plopfile.js

Once we’ve run these commands, we must modify plopfile.js to include the following:

module.exports = plop => {
  plop.setGenerator('component', {
    description: 'Create a component',
    // User input prompts provided as arguments to the template
    prompts: [
      {
        // Raw text input
        type: 'input',
        // Variable name for this input
        name: 'name',
        // Prompt to display on command line
        message: 'What is your component name?'
      },
    ],
    actions: [
      {
        // Add a new file
        type: 'add',
        // Path for the new file
        path: 'src/components/{{pascalCase name}}.js',
        // Handlebars template used to generate content of new file
        templateFile: 'plop-templates/Component.js.hbs',
      },
    ],
  });
};

We’ll also need a component template to specify the content of our generated components. We can create this under plop-templates/Component.js.hbs (the .hbs extension indicates that this is a Handlebars.js template).

import React from 'react';

const {{pascalCase name}} = props => {
  return (
    <div>
      {{pascalCase name}}
    </div>
  );
};

export default {{pascalCase name}};

And lastly, let’s add a script to our package.json to create an alias for the Plop command:

{
  /* ... */
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "generate": "plop"
  },
  /* ... */
}

Here’s the result of this configuration:

Plop.js Configuration Result In Code

You can see when we try to generate a component on the command line, our prompt kicks in, asking the user for a component name. It then uses the value to populate the name parameter in our Handlebars template.

The pascalCase modifier ensures name is formatted in PascalCase, which is creating words by concatenating capitalized words. Here’s a full list of available template helpers.

Adding Plop.js actions

We’re headed in the right direction, but this is still pretty wimpy. Let’s beef up our Plop file to provide some really cool functionality and actions:

module.exports = plop => {
  plop.setGenerator('component', {
    description: 'Create a reusable component',
    prompts: [
      {
        type: 'input',
        name: 'name',
        message: 'What is your component name?'
      },
    ],
    actions: [
      {
        type: 'add',
        // Plop will create directories for us if they do not exist
        // so it's okay to add files in nested locations.
        path: 'src/components/{{pascalCase name}}/{{pascalCase name}}.js',
        templateFile:
          'plop-templates/Component/Component.js.hbs',
      },
      {
        type: 'add',
        path: 'src/components/{{pascalCase name}}/{{pascalCase name}}.test.js',
        templateFile:
          'plop-templates/Component/Component.test.js.hbs',
      },
      {
        type: 'add',
        path:
          'src/components/{{pascalCase name}}/{{pascalCase name}}.module.css',
        templateFile:
          'plop-templates/Component/Component.module.css.hbs',
      },
      {
        type: 'add',
        path: 'src/components/{{pascalCase name}}/index.js',
        templateFile: 'plop-templates/Component/index.js.hbs',
      },
      {
        // Adds an index.js file if it does not already exist
        type: 'add',
        path: 'src/components/index.js',
        templateFile: 'plop-templates/injectable-index.js.hbs',
        // If index.js already exists in this location, skip this action
        skipIfExists: true,
      },
      {
        // Action type 'append' injects a template into an existing file
        type: 'append',
        path: 'src/components/index.js',
        // Pattern tells plop where in the file to inject the template
        pattern: `/* PLOP_INJECT_IMPORT */`,
        template: `import {{pascalCase name}} from './{{pascalCase name}}';`,
      },
      {
        type: 'append',
        path: 'src/components/index.js',
        pattern: `/* PLOP_INJECT_EXPORT */`,
        template: `t{{pascalCase name}},`,
      },
    ],
  })
}

Since we don’t want our generated components represented as just single files, we made them represented as directories packaged with tests, styles, and more.

We also added actions to create a new directory for each new component and instructed Plop to add not just our component file to the directory, but files for tests, styles, and an index to act as an interface for the component directory.

Additionally, we’ve added a couple actions to append to an existing file, components/index.js. This file packages all of the components into a neat little module so we can import them elsewhere using the following:

import { Foo, Bar } from './components'.

Plop actions with type: 'append' search for a pattern within a file and inject a rendered template after the pattern match locations.

For this reason, we must ensure a file exists at components/index.js and then we can append rendered templates at the Plop injection Hooks locations within the file.

Updating the Plop.js templates

Let’s look at the updated templates:

/* PLOP_INJECT_IMPORT */

export {
  /* PLOP_INJECT_EXPORT */
}

In the template above, we see the aforementioned Plop Hooks. Note that these Hooks are totally arbitrary, we just need something in our file to match against patterns defined within our Plop actions.

The remainder of the templates are pretty straightforward because they don’t introduce any new concepts:

import React from 'react';
import PropTypes from 'prop-types';
import styles from './{{pascalCase name}}.module.css';

const {{pascalCase name}} = props => {
  return (
    <div className={styles.root}>
    </div>
  );
};

{{pascalCase name}}.defaultProps = {

};

{{pascalCase name}}.propTypes = {

};

export default {{pascalCase name}};</pre>
<pre>.root {

}
import React from 'react';
import {{pascalCase name}} from './{{pascalCase name}}';

describe('{{pascalCase name}}', () => {
   it('renders without error', () => {

   });
});
import {{pascalCase name}} from './{{pascalCase name}}';

export default {{pascalCase name}};

Although it’s not an issue in this example, be aware that the syntax can conflict between template content and Handlebars.

For example, if there’s a template containing <div style={{ height: 100 }}/>, Handlebars incorrectly interprets the curly braces as expressions. If this happens, you must escape the curly braces as follows:

<div style={{ height: 100 }}/>

With our updated Plop file, our generator is looking pretty slick.

Component Generator Running Code

Building generators for pages, Hooks, and services

Now that we’ve built out our component generator, we can build generators for pages, Hooks, and services as well. Building these other generators doesn’t introduce any new concepts, so I will skip ahead to show our final Plop file:

module.exports = plop => {
  plop.setGenerator('component', {
    description: 'Create a reusable component',
    prompts: [
      {
        type: 'input',
        name: 'name',
        message: 'What is your component name?',
      },
    ],
    actions: [
      {
        type: 'add',
        path: 'src/components/{{pascalCase name}}/{{pascalCase name}}.js',
        templateFile:
          'plop-templates/Component/Component.js.hbs',
      },
      {
        type: 'add',
        path: 'src/components/{{pascalCase name}}/{{pascalCase name}}.test.js',
        templateFile:
          'plop-templates/Component/Component.test.js.hbs',
      },
      {
        type: 'add',
        path:
          'src/components/{{pascalCase name}}/{{pascalCase name}}.module.css',
        templateFile:
          'plop-templates/Component/Component.module.css.hbs',
      },
      {
        type: 'add',
        path: 'src/components/{{pascalCase name}}/index.js',
        templateFile: 'plop-templates/Component/index.js.hbs',
      },
      {
        type: 'add',
        path: 'src/components/index.js',
        templateFile: 'plop-templates/injectable-index.js.hbs',
        skipIfExists: true,
      },
      {
        type: 'append',
        path: 'src/components/index.js',
        pattern: `/* PLOP_INJECT_IMPORT */`,
        template: `import {{pascalCase name}} from './{{pascalCase name}}';`,
      },
      {
        type: 'append',
        path: 'src/components/index.js',
        pattern: `/* PLOP_INJECT_EXPORT */`,
        template: `t{{pascalCase name}},`,
      },
    ],
  })

  plop.setGenerator('page', {
    description: 'Create a page',
    prompts: [
      {
        type: 'input',
        name: 'name',
        message: 'What is your page name?',
      },
    ],
    actions: [
      {
        type: 'add',
        path: 'src/pages/{{pascalCase name}}/{{pascalCase name}}.js',
        templateFile:
          'plop-templates/Page/Page.js.hbs',
      },
      {
        type: 'add',
        path: 'src/pages/{{pascalCase name}}/{{pascalCase name}}.test.js',
        templateFile:
          'plop-templates/Page/Page.test.js.hbs',
      },
      {
        type: 'add',
        path:
          'src/pages/{{pascalCase name}}/{{pascalCase name}}.module.css',
        templateFile:
          'plop-templates/Page/Page.module.css.hbs',
      },
      {
        type: 'add',
        path: 'src/pages/{{pascalCase name}}/index.js',
        templateFile: 'plop-templates/Page/index.js.hbs',
      },
      {
        type: 'add',
        path: 'src/pages/index.js',
        templateFile: 'plop-templates/injectable-index.js.hbs',
        skipIfExists: true,
      },
      {
        type: 'append',
        path: 'src/pages/index.js',
        pattern: `/* PLOP_INJECT_IMPORT */`,
        template: `import {{pascalCase name}} from './{{pascalCase name}}';`,
      },
      {
        type: 'append',
        path: 'src/pages/index.js',
        pattern: `/* PLOP_INJECT_EXPORT */`,
        template: `t{{pascalCase name}},`,
      },
    ],
  })

  plop.setGenerator('service', {
    description: 'Create service',
    prompts: [
      {
        type: 'input',
        name: 'name',
        message: 'What is your service name?',
      },
    ],
    actions: [
      {
        type: 'add',
        path: 'src/services/{{camelCase name}}.js',
        templateFile: 'plop-templates/service.js.hbs',
      },
      {
        type: 'add',
        path: 'src/services/index.js',
        templateFile: 'plop-templates/injectable-index.js.hbs',
        skipIfExists: true,
      },
      {
        type: 'append',
        path: 'src/services/index.js',
        pattern: `/* PLOP_INJECT_IMPORT */`,
        template: `import {{camelCase name}} from './{{camelCase name}}';`,
      },
      {
        type: 'append',
        path: 'src/services/index.js',
        pattern: `/* PLOP_INJECT_EXPORT */`,
        template: `t{{camelCase name}},`,
      }
    ],
  })

  plop.setGenerator('hook', {
    description: 'Create a custom react hook',
    prompts: [
      {
        type: 'input',
        name: 'name',
        message: 'What is your hook name?',
      },
    ],
    actions: [
      {
        type: 'add',
        path: 'src/hooks/{{camelCase name}}.js',
        templateFile: 'plop-templates/hook.js.hbs',
      },
      {
        type: 'add',
        path: 'src/hooks/index.js',
        templateFile: 'plop-templates/injectable-index.js.hbs',
        skipIfExists: true,
      },
      {
        type: 'append',
        path: 'src/hooks/index.js',
        pattern: `/* PLOP_INJECT_IMPORT */`,
        template: `import {{camelCase name}} from './{{camelCase name}}';`,
      },
      {
        type: 'append',
        path: 'src/hooks/index.js',
        pattern: `/* PLOP_INJECT_EXPORT */`,
        template: `t{{camelCase name}},`,
      }
    ],
  })
}

There you have it. This Plop file lets us generate components, pages, Hooks, and services. The above code references many template files; I’ve decided there are too many of them to show them all, but you can view them all in the react-starter-cli repository.

Here’s the final app in action:

Final Plop.js Component Generator Running Code

Conclusion

And that’s how you build a Plop.js app that automatically generates React components.

However, with this highly personalized React CLI, what happens next? Here are some ways you can extend this Plop configuration:

  • Add a prompt to specify whether a component should be a class or functional component
  • Add a prompt to dynamically input a directory where a component should be created
  • Validate user input (for example, minimum length requirement)
  • Populate propTypes and/or defaultProps via prompts
  • Add generators for action types, actions, and reducers
  • Use plop with other frameworks, not just React

Hope this helps, and happy coding!

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Austin Malerba Software Engineer at Target, BS in CS, immature amateur.

3 Replies to “Automatically generate React components with Plop.js”

  1. You might find https://github.com/arminbro/generate-react-cli useful as well.

    Generates react components on the fly. Helps speed up productivity in react projects. For example, you can run one command generate-react component to instantly generate a component with its corresponding files (stylesheet, test).

Leave a Reply