DEV Community

Cover image for Creating a React Component Generator
Matti Bar-Zeev
Matti Bar-Zeev

Posted on

Creating a React Component Generator

In this post join me as I create a generator for a React component. We will be using a really cool tool named Plop and in the end we will be able to spawn new components faster and better.

Introduction

If you appreciate a good Dev experience (DX) and strive to maintain good coding quality/standards, having a tool for generating components scaffold code is a must. Whether you maintain a components library in your organization on not, such a tool will enable you and your colleagues to increase your dev velocity and focus on the real important aspects of component development.

Requirements
So what are we going for in this one?
Well, our generator should create the following:

  • A Component folder with the name given by the developer
  • An index.jsx file for the component
  • An index.css file which the index.jsx file will import with basic generic rules
  • An index.test.js file with a single test that checks the component’s render
  • A Storybook story file which has a single story for the component

That’s quite a lot right there. Let’s get to it


Plop

As mentioned above, one of the tools out there which can help us generate such code is Plop. I find this tool super intuitive with fairly clear documentation.

Going by the Plop documentation, let’s first install it and make sure we can use it:

yarn add plop -D

This is of course a dev dependency.

Let’s create our plopfile.js file right out of the example, using CJS format since the project I’m implementing this for currently does not support the ESM format:

module.exports = function (plop) {
   // create your generators here
   plop.setGenerator('basics', {
       description: 'this is a skeleton plopfile',
       prompts: [], // array of inquirer prompts
       actions: [], // array of actions
   });
};
Enter fullscreen mode Exit fullscreen mode

Before we pour more content to that generator, let’s see if Plop launches as expected by adding an npm script to our package.json and calling it:

"scripts": {
       . . .
       "plop": "plop"
   },
Enter fullscreen mode Exit fullscreen mode

Running yarn plop and… nothing happens. We need to introduce a generator. I’m going back to my plopfile.js and adding this:

plop.setGenerator('React component generator', {
       description: 'A generator for React components',
       prompts: [
           {
               type: 'input',
               name: 'name',
               message: 'Component name',
           },
       ],
   });
Enter fullscreen mode Exit fullscreen mode

Running plop again and I get this nice prompt asking for the component’s name. I gave it the name “Matti” but got this error:

Image description

True. Let’s add an action.
In our action I would like to create a directory by the given component’s name, and set an empty index.jsx file in it (for now). My generator looks like this now:

plop.setGenerator('React component generator', {
       description: 'A generator for React components',
       prompts: [
           {
               type: 'input',
               name: 'name',
               message: 'Component name',
           },
       ],
       actions: [
           {
               type: 'add',
               path: 'src/{{name}}/index.jsx',
           },
       ],
   });
Enter fullscreen mode Exit fullscreen mode

Very intuitive IMO.
So now I have an index.jsx file residing under the Matti directory, which is under the src directory. Perfect.

Let’s fill up the content for this file. In order to do that we’re going to use a Handlebars (yes, an .hbs file, you heard right) template which will allow us to create the content according the component name we gave earlier -

I’m creating a plop-templates/react-component/index.hbs under the root of the project and put the minimal code I need to create a React component:

import React from 'react';
import PropTypes from 'prop-types';

const MyComponent = (props)=> {
   return <div>MyComponent {props.sampleProp}</div>
}

MyComponent.propTypes = {
   sampleProp: PropTypes.number,
};

export default MyComponent;
Enter fullscreen mode Exit fullscreen mode

Notice that I’m not using any dynamic fields in that template yet
Now I will tell the action previously made to use this template when creating a component:

actions: [
           {
               type: 'add',
               path: 'src/{{name}}/index.jsx',
               templateFile: 'plop-templates/react-component/index.hbs',
           },
       ],
Enter fullscreen mode Exit fullscreen mode

Let’s run Plop and see what we get.
Yes, the file is created with the expected content (BTW if the file already exists it will alert on that and will not override it).
It is time to use the given component name inside the template:

import React from 'react';
import PropTypes from 'prop-types';

const {{name}} = (props)=> {
   return <div>{{name}} {props.sampleProp}</div>
}

{{name}}.propTypes = {
   sampleProp: PropTypes.number,
};

export default {{name}};
Enter fullscreen mode Exit fullscreen mode

Much better! After generating the component now the index.jsx file is set correctly.

Checking real fast what we have so far - A Component folder with the name given by the developer and an index.jsx file for the component. Great, moving on to creating this index.css file for this component

I would like the index.css file to contain the class name according to the given component’s name, but in lowercase and for that it would be nice to introduce a helper function which Handlebars can use.
In the plopfile.js I add this:

// Helpers
   plop.setHelper('toLowerCase', function (text) {
       return text.toLowerCase();
   });
Enter fullscreen mode Exit fullscreen mode

Now I will create the index.css template under plop-templates/react-component/index.css.hbs with the following content:

.{{toLowerCase name}} {
   color: aqua;
}
Enter fullscreen mode Exit fullscreen mode

Having the .css template I would like the plop action to generate it along with the index.jsx file. How do I do that?

This requires us to use another Plop api called “addMany” and to change the index.jsx template names a bit. Let’s start first with changing the template name and you will soon understand why -

index.hbs is renamed to index.jsx.hbs

Going to the plopfile, let’s use the “addMany” action like this:

actions: [
           {
               type: 'addMany',
               destination: 'src/{{name}}',
               base: `plop-templates/react-component/`,
               templateFiles: 'plop-templates/react-component/*',
           },
       ],
Enter fullscreen mode Exit fullscreen mode

You might be wondering what’s going on here (and the docs are not really clear on that)
The “destination” is where we want all our files to be generated.
The “templateFiles” (note the little “s” at the end) is where all the templates for this action reside. It was a good thing there to create a directory for each generator.
The “base” is the part we would like to remove from the final file names.

Plop knows to remove the “.hbs” and the base from the final file name and this is why we changed the name of the index file.

Let’s do some little modifications to the index.js.hbs template so that our component will import and use the index.css generated for it:

import React from 'react';
import PropTypes from 'prop-types';
import './index.css';

const {{name}} = (props)=> {
   return <div className="{{toLowerCase name}}">{{name}} {props.sampleProp}</div>
}

{{name}}.propTypes = {
   sampleProp: PropTypes.number,
};

export default {{name}};
Enter fullscreen mode Exit fullscreen mode

Awesome! We have a generated css file with the component imports and uses. Moving on to the test file.
We would like to create a Jest test file which will test the minimal component rendering.

We start by creating a index.test.hbs template at the same location of the rest of the templates:

import React from 'react';
import {render, screen} from '@testing-library/react';
import {{name}} from '.';

describe('{{name}} component', () => {
   it('should render', () => {
       const mockSampleProp = 5;
       const textQuery = `{{name}} ${mockSampleProp}`

       render(<{{name}} sampleProp={mockSampleProp}/>);

       expect(screen.getByText(textQuery)).toBeInTheDocument();
   });
});
Enter fullscreen mode Exit fullscreen mode

We do not need to change anything in our plopfile. This template will be detected and the test file will be generated.
Running Jest to make sure it all passes and indeed - we have success :)

Moving on to the Storybook story, well… you get the drill of it. I’m creating a templated called index.stories.jsx.hbs with the following content:

import React from 'react';
import {{name}} from './index.jsx';

// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
export default {
 title: 'Components/{{name}}',
 component: {{name}},
 // More on argTypes: https://storybook.js.org/docs/react/api/argtypes
 argTypes: {},
};

// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
const Template = (args) => <div><{{name}} {...args} /></div>;

export const Simple = Template.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args
Simple.args = {
   sampleProp:5,
};
Enter fullscreen mode Exit fullscreen mode

Running plop again and the story file is created and displayed on Story book -

Image description

What a marvelous component!

And that, my friends, is it :)

To sum up…

We now have a React component generator which creates 4 files for us with the given component’s name. Any developer can now create a component and get started in a matter of seconds.
Aside from speeding up the component creation process and allowing better DX, this helps to align the components standards in a large organization.
The code can be found on my Pedalboard monorepo code.

As always if you have any question or suggestions on how this can be done better, be sure to leave them in the comments below :)

Hey! If you liked what you've just read check out @mattibarzeev on Twitter 🍻

Photo by KOBU Agency on Unsplash

Top comments (2)

Collapse
 
developerbishwas profile image
Bishwas Bhandari

wow, slick!

Collapse
 
mbarzeev profile image
Matti Bar-Zeev

Cheers mate :)