Keeping up with Create React App (after ejecting)

This is the 3rd edition of the Breathe Life Engineering Blog. This month’s article is written by Kenza Iraki.

At Breathe Life, we love Create React App (CRA). It has allowed us to get new projects started in minutes, rather than days. It’s also given us all a chance to better understand Webpack and our build tools, all at our own pace and with an architecture we could trust from the get-go.

Before CRA, our build system was in bad shape. We were using a forked version of blendid, which had fallen way out of sync with the main module (which itself was already not being maintained much). This version was using webpack 2 and was missing essential features such as tree shaking, making our bundle size enormous (>2mb), and our initial loading time excessive. Those of us with previous experience with CRA were also missing hot reloading and were feeling the impact this architecture had on our productivity. None of us were experts with gulp, webpack and other build tools, and we went through multiple failed attempts at manually upgrading our fork within a reasonable amount of time.

After a few months, we decided it was time to start fresh. I took two days off of feature building to completely get rid of blendid, and replace it with CRA. When I first started, I was hoping we wouldn’t need to eject. I had heard of modules such as react-app-rewired, which allow you to override your webpack config without ejecting. I later realized that they are not recommended by the CRA core team, who say they are brittle and could break your build at any point in time. When Dan Abramov recommends against something, the only wise thing to do is to follow his advice!

The nature of our products makes it difficult to have a one-size-fits-all config. Our products are white-label solutions, which means we need to not only brand the platforms for each carrier, but we also have different sets of questions, validations, and underwriting rules for each carrier. For security purposes, we can’t put everything in a src directory, we need to be able to only import the files and configs we need for the specific carrier being built. For this reason, CRA’s restriction of only being able to import files inside of src was problematic for us.

Along with all of that, we wanted to add our own webpack plugins for various reasons, such as config-webpack, webpack-bundle-analyzer, and lodash-webpack-plugin. Other configs, such as eslint running and failing for minor things such as spacing inconsistencies, would’ve slowed us down considerably (we like running our linting tools on pre-commit hooks instead).

Overall, we felt like we should be able to control our build system and configs, but we still liked the idea of using industry standards set by the CRA team at Facebook. In the end, we had too many moving pieces and use cases that CRA couldn’t cover by default, so after a few hours of fighting with the tools, yarn eject was the only solution left.

The great thing about ejecting is that we all became more comfortable with webpack. We often had to go tinker with the config to get something specific working, and it taught us a lot about how it works and gave us the confidence we were missing when interacting with our build tools.

A few months after integrating CRA, there was a major release executed by the CRA team, adding support for babel 7. For various reasons, we wanted to stay up to date with babel, and wanted to use features such as dynamic imports, which were only available starting in version 7. We contemplated manually upgrading our config to the latest version, but we didn’t want to spend too much time on it, and there were several other additions to CRA that we wanted to have in our build system. One thing that you often hear when people mention ejecting is that it’s an irreversible action. Once you’re out, you’ll become out of sync with the main repo, and there is no way back. We wanted to challenge that idea and made the decision to attempt an upgrade, despite the fact that we had ejected.

Disclaimer

This is the best solution we could find for our needs, it may not work for you, and it’s not ideal by any means, but it gets the job done. We would love your opinion, and if you’ve found a better way, we would definitely love to hear about it!


We started by clearly identifying the modifications we had made over the previous months, so we could separate them from CRA’s default config. For this, we used comment tags:

We also wrapped the default CRA configurations that we had commented out with the same tags, and whenever possible, we added an explanation of why we didn’t want those configs:

After all of our changes were clearly identifiable, we could start the upgrade process.

Step 1

First, we created a new CRA project using npx create-react-app projectName (it’s better to do this in a separate directory).

Step 2

Immediately after that, we ran yarn eject. As a result of this command, we could find a new directory called scripts in our blank project.

Step 3

We moved the contents of the new scripts directory to our main project’s scripts directory, effectively overwriting the existing files.

Step 4

Using git, we applied back the custom config changes that were wrapped with our comments tags.

Step 5

We ran yarn build and yarn start to make sure nothing broke (it rarely happens and in general, this is a pretty smooth process, but if you do get issues, make sure you wrote down the version you’re upgrading from, in case you need to dig a bit into CRA’s issues).


This method requires a little bit of knowledge around webpack, but it’s easy enough that you don’t need to be an expert to use it.

As you can see, this is a pretty manual process at the moment, but we found it to be perfect for our use cases. We contemplated forking CRA’s repo and making changes there, but we have several different products with different configurations, and each needing to import files from different directories outside of src. Not only that, but forking means we have more code to maintain when currently all we need to maintain is a handful of output config files.

This method has come in handy multiple times so far, such as when CRA added TypeScript support, and when TypeScript build times were significantly shortened.

We are working on a developer tool to simplify this process even more, which will parse the config files and take care of applying our changes wrapped around the comment tags to the latest version of CRA. Eventually, maybe we’ll have our own custom config, but at the moment, a slightly tweaked CRA config works perfectly for us.