Using babel-plugin-codegen to generate boilerplate JavaScript code

Photo by Darius Bashar on Unsplash

I came across Kent C. Dodds’ work on babel-plugin-codegen and Babel macros some time ago on the Twitterverse and thought it was cool, but didn’t have a use case at the time. I tried to wedge it somewhere in the back of my memory for future use. That time came over a year later and I could say that memory piped up and said, “Ben, you could use that babel-plugin-codegen right now”. However that didn’t happen. Instead I saw another tweet about babel-plugin-codegen.

Background

When my friend, let’s call him Jake, (because that’s his name) and I started a new project at work we came up with a new file naming convention for React / Redux components. Jake had become a little bothered by what he had seen on projects he had worked on and had come up with an alternative. I added my two cents worth and we finally reached upon the following:

Redux / React component example

Folder structure

Folder structure for AddTask component (Redux connected)

Inside the <component>.connect.js

Example <component>.connect.js file

The <component>.connect.js file contains the functions mapStateToProps and mapDispatchToProps needed for the react-redux connect function. In the case above there was no need for mapStateToProps. It then default-exports the result of calling connect(null, mapDispatchToProps).

index.js for a React / Redux component

Example index.js file for React / Redux component

React component example (without react-redux connect)

Folder structure for TaskListHeading component (not Redux connected)

index.js for a React component (without react-redux connect)

Example index.js for React component (without Redux)

The component index.js file acts a glue between a component and the react-redux connect function exporting the result, or it just exports the component if there is no need for Redux. In the case where there is no Redux it might be tempting to make any consuming component import the component directly. However importing components in the same way was preferred over a little bit of boilerplate code. So regardless of whether a component required Redux or not, the import was the same as you can see below:

If we had taken the approach to not include an index.js for a component without Redux we would have the following, which means you would need to know whether a component was Redux connected or not before importing it.

Separating a component file structure into *.component.js, *.connect.js, *.styles.js and index.js made for a clear separation of concerns but also aided searching for files in the browser and in the IDE.

You said something about code generation, where is it then?

When we agreed upon this approach to component file structure we did comment on the index.js file being just boilerplate code. Once set up you probably won’t need to revisit it often, only maybe when you realise you need to hook it up to the Redux store or you need to detach from Redux as part of some refactoring. So it would be useful if you could auto-generate the relevant imports and export, and regenerate it any time you added/removed the *.connect.js file. This is where babel-plugin-codegen comes in.

babel-plugin-codegen

The plugin runs at build time (in my case via webpack / babel-loader), generating a string of code from code and injecting it where you specified. The following shows the before and after for a component index.js file:

Before/after component index.js file

By using the /* codegen */ comment the plugin knows to execute the code in generateComponentIndex.js at build time passing in the Node.js value of __dirname (directory name of the current module), and finally injecting the return value of generateComponentIndex.js in place of the import statement containing /* codegen */.

Generate relevant import / exports

generateComponentIndex.js uses the Node.js File System (fs) module for getting a list of files for the directory passed in as an argument. In our case this will be the directory for whichever component is being evaluated by babel-loader / webpack. With babel-plugin-codegen you can use any Node.js module that runs synchronously.

Output from webpack-dev-server with babel-plugin-codegen

To demonstrate it working I’ve built a task list. Yes, this is just another to-do list but I like the words task list because to-do just sounds like you have to do them but a task list, well it doesn’t sound like it’s forcing you do it. You can find the links to the repo and the working task list example at the end.

Caveat — if you refactor a component to either add Redux (adding a *.connect.js file) or remove Redux (removing a *.connect.js file), in order for webpack-dev-sever to pick up your changes you need to edit the index.js file to trigger a rebuild. I add an additional space character usually. When using webpack to produce a production build this isn’t an issue because it will execute any babel-plugin-codegen code every time.

Does it impact build time?

Not much.

Photo by SpaceX on Unsplash — Just like my experiment :)

I did an experiment. Well, sort of. I created two React projects each with one hundred very basic components, one using babel-plugin-codegen and one without. For each project I recorded how long it took to start webpack-dev-server straight after installing fresh dependencies (cold start), how long it took to start webpack-dev-server after a previous start (warm start), and finally how long it took webpack-dev-server to recompile when a component’s index.js file had changed. I repeated these tasks 5 times and took a “scientific” average.

The project not using babel-plugin-codegen performed slightly quicker for each task on average. For the cold start test the project not using babel-plugin-codegen was 0.1758 seconds faster than the project using babel-plugin-codegen, 0.0171 seconds faster for the warm start test, and 0.015 seconds quicker for the individual file change test.

On previous runs of the experiment sometimes the project using the plugin performed slightly better.

Further use cases

As stated on the babel-plugin-codegen README, “The applications of this plugin are wide”. In my example I have also used it to enforce filename conventions of each component related file, making sure each filename began with a lowercase character.

Example additional logic (this time file naming convention validation)

With this extra snippet of code, webpack will fail to compile if it catches an error (in the case below I changed addTask.component.js to AddTask.component.js).

Webpack compilation failing with validation check in codegen file

babel-plugin-codegen is very powerful. Used sparingly, it could help in many use cases. But used too broadly could result in code that is harder to understand. Good documentation in the form of comments greatly helps here. While our use-case might not be perfect, it helped us to reduce some rarely visited boilerplate code as well as giving us the excuse to try out something new-and-cool.

Wrapping up

To find out more about babel-plugin-codegen head over to https://github.com/kentcdodds/babel-plugin-codegen and also check out https://github.com/kentcdodds/babel-plugin-macros

The code samples referenced above (and the Task list) can be found here https://github.com/mrbenhowl/auto-gen-boilerplate-code-babel-macro-codegen and here https://mrbenhowl.github.io/auto-gen-boilerplate-code-babel-macro-codegen/.

The results of my really scientific experiment 😉 can be found here and here.

PS — If you don’t already follow Kent on Twitter (https://twitter.com/kentcdodds) I highly recommend you do, for his fantastic work and that he seems like a overall nice guy.

Thanks for reading. Have a good day 😁 🇦🇺