Beyond create-react-app (CRA)

Tarik Huber
codeburst
Published in
12 min readNov 16, 2017

--

What can we do to make the development after creating an application with create-react-app (CRA) as easy as using CRA itself?

The time before CRA

Do you remember the time before CRA? I do. A very frustrating period. Especially because it was the time I started working with react. About 80 % of that frustration was not because of react. The main amount of time I was just figuring out how to setup all that environment to work as it should. As a novice in the node community it was hard to find some documentation, tutorial or other kind of instruction that was not outdated.

But after a while, even I didn’t understand what webpack, babel, and others were doing, it started to work. Then the fun started. Working with React is such a releasing experience for me. Unfortunately, the fun didn’t last long.

One of those loose, coupled components in my environment got an update and everything broke apart. The frustration continued. With time I mastered the working setup for my projects.

There is a lot of boilerplate out there, but in most cases the repositories are abandoned, outdated or even overfilled with features I didn’t like. Still, there was a problem with updates.

Something still didn’t make sense. After updates of the boilerplate, you have to do more changes on every project than you would like to. Using a boilerplate is supposed to mean an easy start, but after that your on your own.

But there is always a light in the end of the tunnel. Finding CRA was a real game-changer for me.

Working with CRA

I know there are a lot of developers that don’t like to use CRA. I understand that. There are a lot of use-cases where you need special configs to make your stuff work. For 99% of my use cases, CRA is enough. To not waste time on the question “should you use CRA or not” let us continue to the part on “how it is to use CRA”.

Well since using CRA the main part of my frustration faded away. As a developer, there is always some frustration for something from time to time. If not, you should consider if you are using your potential enough and if you are working on tasks and projects that will exhaust your skills to the edge. For me that is the only or best way to develop your own skills. But we should not reinvent the wheel again and again. CRA solved a huge problem and as long as I don’t find any problem with it, I will use it as it is.

What is the main benefit of using CRA and not other boilerplate's? For me it’s the capability to get updates without breaking my whole project. Everything is in scripts that can be updated as easy as every other npm module. The large community and the Facebook team working on CRA makes me comfortable that this tool will not get abandoned or outdated.

CRA solved the problem of wasting time on the decisions and effort you had to put forth to make your application environment work. But what do we have after the creation of a project using CRA? We are left on our own again.

We get a minimal example of a web page. Without routing, internationalization, basic menu or other features most modern web applications need. That is exactly how CRA should work! It enables us to make from it everything we want and not everyone needs all that features. That is the point where CRA stopped and that’s exactly the point they had to stop to make the tool fit for the needs of the majority of developers.

But what can we do now? After this kind of elegant setup coming from a experienced team of developers where you barely see how and where all the “magic” happens. Now you have to setup your application with routing, internationalization, code splitting, signup and sign in components, etc… Of course this leads us to our next part.

Project Boilerplate’s

We are developers. We want to stay DRY not just in code but also in our whole working environment and approach. Do to so some of us started making boilerplate for react projects. Some without CRA and from scratch and others by using CRA as base and adding the boilerplate on the place where CRA stops. So did I.

I created a boilerplate project called React-Most-Wanted (RMW)(source code) , which should have features that most projects need. That’s where the name comes from ;) I even wrote an article on that. I’m not writing about it now to make it popular or to bring more developers to it. It is a first version of the solution this article is trying to describe.

At first, this project was just a boilerplate for my own projects with no intent on making it work for others. Still I made a demo application and with some time more and more developers signed up and tried it out. The feedback was amazing. And contributors started helping to make it even better.

I tried to use every library like react-redux, react-router, react-intl and others in their pure way. Without any customization or helper libraries like react-router-redux. It worked out and I hope that RMW has a clean and clear structure prepared for applications that can scale and be understood by developers starting to work with react and the node environment.

React Most Wanted Logo

Some developers don’t like my decision to use Firebase as the application backend. To be honest, I started using it because it allowed me to make a demo application without hosting servers on my own and also have all authentication features without much effort. With time I liked it more and more and now I’m using it for every new project I make. Even creating my own libraries like firekit that makes the sync of Firebase and redux much easier. Somehow I have the feeling that there are just two types of developers: Those who hate and those who love Firebase (and maybe those who don’t know about it). I’m one of those who love it :)

RMW is also a Progressive Web Application (PWA). It has integrated push notifications, offline support and a fast first load. To make the first load fast we had to integrate code splitting. It reduces the main.js file to 240 kb no matter how many routes you add to your project.

Here we are. A nice boilerplate that has everything we need. I use it in a dozen projects and even for demo applications in some of my npm libraries. And i know that a lot of other developers use it as a boilerplate for their projects.

Some developers (including me) could break the project by changing main parts of it like the Root , AppLayout or Drawer Component . In most cases the change was not intended or just made by mistake. There should be a boilerplate where a developer can break it only by intended changes to the main parts of it.

By maintaining all of those projects to have the same code ended up by copy-pasting every change from the main RMW project to all others. Even I have splitted some core parts to standalone npm modules some code was still left over and it was the skeleton or shell of the whole project.

If some dependencies get an major update and I have to change the structure of the application shell drastically it would mean to go to every other project and make the same change over and over again. In such major dependency updates the changes often must be done in more than one file and those files often have some configs specific to the project so just copy-pasting the whole file was no option. The frustration started to rise exponentially to the amount of projects I had to maintain.

Something still didn’t make sense. After updates of the boilerplate, you have to do more changes on every project than you would like to. Using a boilerplate is supposed to mean an easy start, but after that your on your own.

Does the text above sound familiar? It’s the same i wrote above in the part for project environment. We need for the part after CRA a solution that is like CRA. It should have a default application shell that you can adjust for your needs. I was thinking for weeks on how to make such a solution. After struggling with some main concepts on how to do it I just decided to start working on it and fighting with the problems on the way.

Beyond CRA with rmw-shell

The plan

The plan was to create a npm module that you could import at the CRA index.js file and have a working application. Something like this:

import React from 'react'
import ReactDOM from 'react-dom'
import registerServiceWorker from './registerServiceWorker'
import App from 'some-awesome-module'
ReactDOM.render(
<App />
, document.getElementById('root')
)
registerServiceWorker()

It should also allow to adjust everything for your own needs. The developer using it should be able add his own i18n, theming, routes and even to override the existing default routes, menu items, locales and themes. At first it seemed easy to do.

Bringing it to reality

The main obstacle was to obtain the code splitting from the npm module. That caused some other problems like the Firebase import. Firebase has a really large npm module. Even compressed it would make over 50 % of the main.js file. So to reduce that file we moved the firebase load to our routing where the code splitting is done. All chunks would be significantly larger than before but they would be lazy loaded so it won’t be noticed by the user. That approach worked out when we used it in the boilerplate RMW before. Now in the rmw-shell it was a problem. The firebase.js file where we initialize the Firebase app can’t just be imported and send to the App component. The solution was straightforward. We had to send a import function like this:

() => import('./firebase')

This would load into our npm module the firebase initialization from our project where we use it.

Besides some other small and large problems during the development of rmw-shell it worked out in the end. We are now able to use CRA to create a new project, import rmw-shell and add it’s App component to have a full functional PWA application. And because we are using code splitting the size of the main.js file will stay 240 kb no matter how many routes you add to the project. It’s as easy as this:

import React from 'react'
import ReactDOM from 'react-dom'
import registerServiceWorker from 'rmw-shell/lib/registerServiceWorker'
import App from 'rmw-shell/lib/App'
ReactDOM.render(
<App />
, document.getElementById('root')
)
registerServiceWorker()

Maybe you already noticed that we don’t import the App and registerServiceWorker direct from our rmw-shell . We import it throughout the lib folder the get only the part we need. The reason for this is that we want to reduce our main.js file size and rmw-shell is exporting some parts that would blow it up.

The reason this is working out with zero config is that rmw-shell has fallback configs. If you don’t provide a configuration it will use the default one. Even the Firebase configs and initialization will fallback to those in rmw-shell.

The main reason I decided to make this work with zero config was one of many videos and articles with and from Dan Abramov. The video below was the one that inspired me to make all this work and effort to finish this project even I’m now in the forth day struggling to make it work with zero config. Even Dan is talking about other tools to make the live of JavaScript developers easier I see there a lot of similarities.

Making it your application

Now that we have our application with all default configs let us see how we can configure this into our own application.

The main config that we can send to our rmw-shell application component are the routes. We can see through the RMW source code that we need to send just an array of predefined async components to our application configs.

import React from 'react'
import React from 'react'
import makeLoadable from 'rmw-shell/lib/containers/MyLoadable'
import RestrictedRoute from 'rmw-shell/lib/containers/RestrictedRoute'
const MyLoadable = (opts, preloadComponents) => makeLoadable({ ...opts, firebase: () => import('./firebase') }, preloadComponents)const AsyncDashboard = MyLoadable({ loader: () => import('./containers/Dashboard/Dashboard') });const AsyncDocument = MyLoadable({ loader: () => import('./containers/Document/Document') });const AsyncCollection = MyLoadable({ loader: () => import('./containers/Collection/Collection') });const AsyncAbout = MyLoadable({ loader: () => import('./containers/About/About') });const AsyncTask = MyLoadable({ loader: () => import('./containers/Tasks/Task') });const AsyncTasks = MyLoadable({ loader: () => import('./containers/Tasks/Tasks') }, [AsyncTask]);const AsyncCompany = MyLoadable({ loader: () => import('./containers/Companies/Company') });const AsyncCompanies = MyLoadable({ loader: () => import('./containers/Companies/Companies') }, [AsyncCompany]);const routes = [
<RestrictedRoute type='private' path="/" exact component={AsyncDashboard} />,
<RestrictedRoute type='private' path="/dashboard" exact component={AsyncDashboard} />,
<RestrictedRoute type='private' path="/tasks" exact component={AsyncTasks} />,
<RestrictedRoute type='private' path="/tasks/edit/:uid" exact component={AsyncTask} />,
<RestrictedRoute type='private' path="/tasks/create" exact component={AsyncTask} />,
<RestrictedRoute type='private' path="/companies" exact component={AsyncCompanies} />,
<RestrictedRoute type='private' path="/companies/edit/:uid" exact component={AsyncCompany} />,
<RestrictedRoute type='private' path="/companies/create" exact component={AsyncCompany} />,
<RestrictedRoute type='private' path="/about" exact component={AsyncAbout} />,
<RestrictedRoute type='private' path="/document" exact component={AsyncDocument} />,
<RestrictedRoute type='private' path="/collection" exact component={AsyncCollection} />,
]
export default routes;

Just like the routes we can send the menuItems , initial_state , locales , themes and others. It is possible that other configs could be added to provide a even better configuration.

Here we can see the config file from the RMW demo project:

import getMenuItems from './menuItems'
import locales from './locales'
import routes from './routes'
import themes from './themes'
const config = {
firebase_config: {
apiKey: 'AIzaSyBQAmNJ2DbRyw8PqdmNWlePYtMP0hUcjpY',
authDomain: 'react-most-wanted-3b1b2.firebaseapp.com',
databaseURL: 'https://react-most-wanted-3b1b2.firebaseio.com',
projectId: 'react-most-wanted-3b1b2',
storageBucket: 'react-most-wanted-3b1b2.appspot.com',
messagingSenderId: '258373383650'
},
firebase_config_dev: {
apiKey: 'AIzaSyB31cMH9nJnERC1WCWA7lQHnY08voLs-Z0',
authDomain: 'react-most-wanted-dev.firebaseapp.com',
databaseURL: 'https://react-most-wanted-dev.firebaseio.com',
projectId: 'react-most-wanted-dev',
storageBucket: 'react-most-wanted-dev.appspot.com',
messagingSenderId: '70650394824'
},
firebase_providers: [
'google.com',
'facebook.com',
'twitter.com',
'github.com',
'password',
'phone'
],
initial_state: {
theme: 'dark',
locale: 'en'
},
drawer_width: 256,
locales,
themes,
routes,
getMenuItems,
firebaseLoad: () => import('./firebase')
}
export default config

It is important to say that every config sent to the application will override the default. The i18n config is a little bit different. The default translations will stay even if you add your own locale configuration that doesn’t contain those but by providing a translation that already exists in the default project you will override it. More of the configuration options can be found in the project description (those are still in the works). Her we can see how a fully customized implementation of rmw-shell looks like in CRA:

import App from 'rmw-shell/lib/App'
import React from 'react'
import ReactDOM from 'react-dom'
import configureStore from './store'
import config from './config'
import locales from './locales'
import registerServiceWorker from 'rmw-shell/lib/registerServiceWorker'
import { addLocalizationData } from 'rmw-shell/lib/locales'
addLocalizationData(locales)ReactDOM.render(
<App appConfig={{ configureStore, ...config }} />
, document.getElementById('root')
)
registerServiceWorker()

The config file in the RMW demo project looks exactly the same as the fallback config in rmw-shell . For more insights just check out the source code of the RMW demo project. And not to forget the source code of rmw-shell.

The possibilities

This project demonstrates what is possible to create with using just the tools we already have. I can imagine a similar project for a more generic use-case. Where the material-ui, Firebase and other RMW specific parts are stripped away. We could then have a small library providing us with at least the routing with code splitting and internationalization. Boilerplate projects are very helpful for those who are starting to dive into the node and react world. But they can lead us into situations where developers learn to use outdated code, examples and practices. We can’t always stay up to date with the fast evolving node community but we can make tools that should make it at least easier.

I hope that this project and article can inspire others to create their own shell modules or help us make this one better. I would love to hear your feedback on this by writing a comment below. Any critic, correction and suggestion is welcome.

If you want to keep up with my new articles you can follow me on Twitter here.

--

--

Organiser of Google Developer Group Berchtesgadener Land • Creator of React Most Wanted • Working at ICS Logistik • External teacher at UAS Salzburg