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
-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",
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
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);
};
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"
}
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" }
]
}
],
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`));
};
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;
}
}
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, titledadd
or modify an existing file viaappend
, -
path
- location of the created or modified component -
templateFile
- a path to handlebars template used to create or modify a file. Alternatively atemplate
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 }};
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.
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)
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'.
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!
Nice spot! I've edited the article, thanks for the hint :)
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?