Editor’s note: This post was on 28 October 2021. 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.
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.
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.
A few things to note about this setup:
components
holds our reusable React componentspages
holds single-use React components (most often rendered by routes)hooks
holds our custom React Hooksservices
holds stateful business logic servicesindex.js
files expose interfaces at the directory levelNow 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
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:
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.
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.
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.
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:
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:
propTypes
and/or defaultProps
via promptsHope this helps, and happy coding!
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
3 Replies to "Automatically generate React components with Plop.js"
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).
This was a great read and I enjoyed every bit of it. I personally didn’t know of “Blog name Generators” which of course is totally useful and makes sense. i have https://shipnamegenerator.info/ tried this after visiting your site.
User snippets are really powerful check them out https://code.visualstudio.com/docs/editor/userdefinedsnippets