DEV Community

Cover image for Don't use create-react-app: How you can set up your own reactjs boilerplate.
Nikhil Kumaran S
Nikhil Kumaran S

Posted on

Don't use create-react-app: How you can set up your own reactjs boilerplate.

What is CRA?

Create React App is a toolchain built and maintained by developers at Facebook for bootstrapping React applications. You simply run one command and Create React App sets up the tools you need to start your React project.

Advantages of CRA

  • Get started with a single command
npx create-react-app my-app
Enter fullscreen mode Exit fullscreen mode
  • Less to Learn. You can just focus on React alone and don't have to worry about webpack, babel, and other such build dependencies.
  • Only one build dependency react-scripts. This maintains all your build dependencies, so it's easy to maintain and upgrade with just one command.
npm install react-scripts@latest
Enter fullscreen mode Exit fullscreen mode

Disadvantages of CRA

  • Difficult to add custom build configs. One way to add custom configs is to eject the app, but then it overrides the Only one build dependency advantage. The other way is you can use packages like customize-cra or react-app-rewired but then they have limited capabilities.
  • Abstracts everything. It's important to understand the things that need to run a React app. But due to it's Only one build dependency advantage, a beginner might think that react-scripts is the only dependency needed to run react apps and might not know that transpiler(babel), bundler(webpack) are the key dependencies which are used under the hood by react-scripts. This happened to me until I read this awesome article.
  • CRA is bloated - IMO. For example, CRA comes with SASS support, if you are using plain CSS or Less it's an extra dependency that you will never use. Here is a package.json of an ejected CRA app.

The alternative for CRA is to set up your own boilerplate. The only advantage that we can take from CRA is Get started with a single command and we can eliminate all of its disadvantages by setting up dependencies and configs by ourselves. We cannot take the other two advantages because it introduces two disadvantages(Abstracts everything and Difficult to add custom build configs).

This repo has all the code used in this blog post.

First, initialize your project with npm and git

npm init
git init
Enter fullscreen mode Exit fullscreen mode

Let's quickly create a .gitignore file to ignore the following folders

node_modules
build
Enter fullscreen mode Exit fullscreen mode

Now, let's look at what are the basic dependencies that are needed to run a React app.

react and react-dom

These are the only two runtime dependencies you need.

npm install react react-dom --save
Enter fullscreen mode Exit fullscreen mode

Transpiler(Babel)

Transpiler converts ECMAScript 2015+ code into a backward-compatible version of JavaScript in current and older browsers. We also use this to transpile JSX by adding presets.

npm install @babel/core @babel/preset-env @babel/preset-react --save-dev 
Enter fullscreen mode Exit fullscreen mode

A simple babel config for a React app looks like this. You can add this config in .babelrc file or as a property in package.json.

{
    "presets": [
        "@babel/preset-env",
        "@babel/preset-react"
    ]
}
Enter fullscreen mode Exit fullscreen mode

You can add various presets and plugins based on your need.

Bundler(Webpack)

Bundler bundles your code and all its dependencies together in one bundle file(or more if you use code splitting).

npm install webpack webpack-cli webpack-dev-server babel-loader css-loader style-loader html-webpack-plugin --save-dev 
Enter fullscreen mode Exit fullscreen mode

A simple webpack.config.js for React application looks like this.

const path = require('path');
const HtmlWebPackPlugin = require('html-webpack-plugin');

module.exports = {
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: 'bundle.js',
  },
  resolve: {
    modules: [path.join(__dirname, 'src'), 'node_modules'],
    alias: {
      react: path.join(__dirname, 'node_modules', 'react'),
    },
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
        },
      },
      {
        test: /\.css$/,
        use: [
          {
            loader: 'style-loader',
          },
          {
            loader: 'css-loader',
          },
        ],
      },
    ],
  },
  plugins: [
    new HtmlWebPackPlugin({
      template: './src/index.html',
    }),
  ],
};
Enter fullscreen mode Exit fullscreen mode

You can add various loaders based on your need. Check out my blog post on webpack optimizations where I talk about various webpack configs that you can add to make your React app production-ready.

That is all the dependencies we need. Now let's add an HTML template file and a react component.

Let's create src folder and add index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>React Boilerplate</title>
</head>
<body>
  <div id="root"></div>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Let's create a HelloWorld.js react component in the src folder

import React from 'react';

const HelloWorld = () => {
  return (
      <h3>Hello World</h3>
  );
};

export default HelloWorld;
Enter fullscreen mode Exit fullscreen mode

Let's add index.js file to the src folder

import React from 'react';
import { render } from 'react-dom';

import HelloWorld from './HelloWorld';

render(<HelloWorld />, document.getElementById('root'));
Enter fullscreen mode Exit fullscreen mode

Finally, let's add the start and build scripts in package.json

"scripts": {
    "start": "webpack-dev-server --mode=development --open --hot",
    "build": "webpack --mode=production"
  }
Enter fullscreen mode Exit fullscreen mode

That is it. Now our react app is ready to run. Try the commands npm start and npm run build.

Now, let's implement the Get started with a single command advantage from CRA. Basically, we are going to use an executable JS file that runs when we type a specific command(your boilerplate name) in the command line. Eg. reactjs-boilerplate new-project For this, we are going to use bin property in package.json.

Let's first create the executable JS file. Install fs-extra

npm i fs-extra
Enter fullscreen mode Exit fullscreen mode

Create bin/start.js file on your project root with the following content.

#!/usr/bin/env node
const fs = require("fs-extra");
const path = require("path");
const https = require("https");
const { exec } = require("child_process");

const packageJson = require("../package.json");

const scripts = `"start": "webpack-dev-server --mode=development --open --hot",
"build": "webpack --mode=production"`;

const babel = `"babel": ${JSON.stringify(packageJson.babel)}`;

const getDeps = (deps) =>
  Object.entries(deps)
    .map((dep) => `${dep[0]}@${dep[1]}`)
    .toString()
    .replace(/,/g, " ")
    .replace(/^/g, "")
    // exclude the dependency only used in this file, nor relevant to the boilerplate
    .replace(/fs-extra[^\s]+/g, "");

console.log("Initializing project..");

// create folder and initialize npm
exec(
  `mkdir ${process.argv[2]} && cd ${process.argv[2]} && npm init -f`,
  (initErr, initStdout, initStderr) => {
    if (initErr) {
      console.error(`Everything was fine, then it wasn't:
    ${initErr}`);
      return;
    }
    const packageJSON = `${process.argv[2]}/package.json`;
    // replace the default scripts
    fs.readFile(packageJSON, (err, file) => {
      if (err) throw err;
      const data = file
        .toString()
        .replace(
          '"test": "echo \\"Error: no test specified\\" && exit 1"',
          scripts
        )
        .replace('"keywords": []', babel);
      fs.writeFile(packageJSON, data, (err2) => err2 || true);
    });

    const filesToCopy = ["webpack.config.js"];

    for (let i = 0; i < filesToCopy.length; i += 1) {
      fs.createReadStream(path.join(__dirname, `../${filesToCopy[i]}`)).pipe(
        fs.createWriteStream(`${process.argv[2]}/${filesToCopy[i]}`)
      );
    }

    // npm will remove the .gitignore file when the package is installed, therefore it cannot be copied, locally and needs to be downloaded. Use your raw .gitignore once you pushed your code to GitHub.
    https.get(
      "https://raw.githubusercontent.com/Nikhil-Kumaran/reactjs-boilerplate/master/.gitignore",
      (res) => {
        res.setEncoding("utf8");
        let body = "";
        res.on("data", (data) => {
          body += data;
        });
        res.on("end", () => {
          fs.writeFile(
            `${process.argv[2]}/.gitignore`,
            body,
            { encoding: "utf-8" },
            (err) => {
              if (err) throw err;
            }
          );
        });
      }
    );

    console.log("npm init -- done\n");

    // installing dependencies
    console.log("Installing deps -- it might take a few minutes..");
    const devDeps = getDeps(packageJson.devDependencies);
    const deps = getDeps(packageJson.dependencies);
    exec(
      `cd ${process.argv[2]} && git init && node -v && npm -v && npm i -D ${devDeps} && npm i -S ${deps}`,
      (npmErr, npmStdout, npmStderr) => {
        if (npmErr) {
          console.error(`Some error while installing dependencies
      ${npmErr}`);
          return;
        }
        console.log(npmStdout);
        console.log("Dependencies installed");

        console.log("Copying additional files..");
        // copy additional source files
        fs.copy(path.join(__dirname, "../src"), `${process.argv[2]}/src`)
          .then(() =>
            console.log(
              `All done!\n\nYour project is now ready\n\nUse the below command to run the app.\n\ncd ${process.argv[2]}\nnpm start`
            )
          )
          .catch((err) => console.error(err));
      }
    );
  }
);

Enter fullscreen mode Exit fullscreen mode

Now let's map the executable JS file with a command. Paste this in your package.json

"bin": {
    "your-boilerplate-name": "./bin/start.js"
  }

Enter fullscreen mode Exit fullscreen mode

Now let's link the package(boilerplate) locally by running

npm link
Enter fullscreen mode Exit fullscreen mode

Now, when this command is typed in the terminal(command prompt), your-boilerplate-name my-app, our start.js executable is invoked and it creates a new folder named my-app, copies package.json, webpack.config.js, gitignore, src/ and installs the dependencies inside my-app project.

Great, now this works in your local. You can bootstrap React projects(with your own build configs) with just a single command.

You can also go one step further and publish your boilerplate to npm registry. First, commit and push your code to GitHub and follow these instructions.

Hurray! We created our alternative to create-react-app within a few minutes, which is not bloated(you can add dependencies as per your requirement) and easier to add/modify build configs.

Of course, our set up is very minimal, and it's certainly not ready for production. You have to add a few more webpack configs to optimize your build.

I've created a reactjs-boilerplate with the production-ready build set up, with linters and pre-commit hooks. Give it a try. Suggestions and contributions are welcome.

Recap

  • We saw the advantages and disadvantages of CRA.
  • We decided to take Get started with a single command advantage from CRA and implement it in our project and eliminate all of its drawbacks.
  • We added minimal webpack and babel configs required to run a react application
  • We created a HelloWorld.js react component, ran it using dev server, and build it.
  • We created an executable JS file and mapped it with a command name via bin property in the package.json.
  • We used npm link to link our boilerplate and made our boilerplate to bootstrap new react projects with a single command.

That's it, folks, Thanks for reading this blog post. Hope it's been useful for you. Please do comment your questions and suggestions.

References

Latest comments (63)

Collapse
 
azizoid profile image
Aziz

I do not understand passion of some developers to create abstractions on everything. that is a trap, when you end up creating abstraction on already existing abstraction.
If you need to create abstraction to an abstraction probably you are doing something wrong.

Less to Learn

Time is a limited resource. I do not have time to learn how webpack or babel works, when i need to finish my ticket with has nothing to do with it. if workspace is set and everything configured, yo do not need to install webpack or babel.

Again this is a developer trap when some devs try to do everything that is done for them before them by themselves.

Why? just to showoff how cool you are?

Collapse
 
narendraktw profile image
Narendra Bisht
Collapse
 
adarshgoyal profile image
Adarsh Goyal

nice one!

Collapse
 
tolkienfan2 profile image
tolkienfan2

Thanks for this - it was very helpful in creating a new React app minus the bloat.

I ran into a problem creating my own boilerplate though - start.js at line 12 produces an error (line 12: const babel = "babel": ${JSON.stringify(packageJson.babel)})

Some error while installing dependencies
Error: Command failed: cd react-test && git init && node -v && npm -v && npm i -D @babel/core@^7.16.7 @babel/preset-env@^7.16.7 @babel/preset-react@^7.16.7 babel-loader@^8.2.3 css-loader@^6.5.1 html-webpack-plugin@^5.5.0 style-loader@^3.3.1 webpack@^5.65.0 webpack-cli@^4.9.1 webpack-dev-server@^4.7.2 && npm i -S react@^17.0.2 react-dom@^17.0.2
npm ERR! code EJSONPARSE
npm ERR! path C:\Users\mang\PycharmProjects\react-test/package.json
npm ERR! JSON.parse Unexpected token "u" (0x75) in JSON at position 226 while parsing near "...on\"\n },\n \"babel\": undefined,\n \"author..."
npm ERR! JSON.parse Failed to parse JSON data.
npm ERR! JSON.parse Note: package.json must be actual JSON, not just JavaScript.

Collapse
 
daehyeonmun2021 profile image
Daehyeon Mun

Hello, I'm a Junior FE dev from S.Korea. I just signed up here to say "Thank you"! to you. There are some issues at work for me. The project I'm building at work is really heavy. It's built with CRA by other person and you know there are many dependencies I never use in the boilerplate and I need to learn how to handle bundle size for the future. So I really needed to learn how to set up react project with webpack from A to Z to make its performance better and maintain potential future conflict well. And this post really helped me a lot. I learned a lot from this post. I'm sure the knowledge I learned from here will make my work better! Now, I can build react project from Zero. Thank you so much!

Collapse
 
nikhilkumaran profile image
Nikhil Kumaran S

Thanks for the feedback. I'm glad you find it useful.

Collapse
 
c0dezer019 profile image
Brian Blankenship

Idk about before, but with the new WebPack you don't need the bin/start. That's pretty useless now and does nothing. Also the command moves from webpack-dev-server to webpack serve (still need that as a dep though).

Collapse
 
nikhilkumaran profile image
Nikhil Kumaran S

Thanks for pointing it out.

Collapse
 
joejavacavalier2001 profile image
roger.k.trussell@gmail.com • Edited

In the webpack.config.js file, in the resolve object, you might want to include "extensions: ['.js', '.jsx']" after defining "alias". I had a hard time getting webpack to find and transpile my jsx files until I did that.

You need to define those extensions IN ADDITION to defining rules to deal with those extensions.

Thanks though for teaching use another way besides CRA. CRA was adding dependencies that would make npm freak out sometimes.

Collapse
 
nikhilkumaran profile image
Nikhil Kumaran S

Thanks for the suggestion. Will update.

Collapse
 
rohimchou profile image
RohimChou • Edited

Trying to follow through this post, however I encountered Error: Cannot find module 'webpack-cli/bin/config-yargs' error while running the npm start.

Changing from
"start": "webpack-dev-server --mode=development --open --hot"
To
"start": "webpack serve --mode=development --open --hot"
resolved the issue.

Seems like the interface in not compatible with webpack5 now. webpack-dev-server

Collapse
 
nikhilkumaran profile image
Nikhil Kumaran S

Thanks. Will update it.

Collapse
 
talkohavy profile image
talkohavy

Yeah, i've also encountered that issue (and solved it the same way).
Which btw? Is exactly the argument some people made in the comments above in regards for high maintainance, and error prone code when NOT using CRA, and going for pure Webpack solution.

Collapse
 
nikhilkumaran profile image
Nikhil Kumaran S • Edited

Yeah, but migration to newer versions won't be that difficult if you have the habit of checking for any potential refactoring in your project regularly.

Collapse
 
rokstar profile image
Roman Kirsanov

CRA is devil! This is good article and I totally agree.

Here is my thoughts:

  • CRA is very bloated, it creates 16K of files in your node_modules folder;
  • CRA forces you to use many things that you don't need;
  • CRA does not allow you to use external shared typescript code as it maintains tsconfig.json for you and does not allow you to add some props to it, for example you cannot use "paths" property as CRA just resets it on every start. It does not even allow you to reference code outside your src, what???
  • CRA is only for SPA and if you wanna keep frontend and backend in the same repository (monorepo)... so you will have to hack around it to avoid duplication of node_modules folder etc... (yarn workspaces solve this problem, but it is unnessecary time waste)
  • CRA is only for SPA and if your project suppose to have several apps you will meet the same problems as above + when deploying all the apps to the same, say, nginx container you will hit the problem where react's static folders override each other, or you have to mess around with nginx config to split things up... what a mess....

CRA is toy for dummies, it creates noise in frontend community and some folks think it's a best practies and afraid to admit that they spend days hacking around this crap...

Yes, you can learn how to cook it, and there are many people who will argue with me, but I will never admit that CRA is efficient and reliable way to run project... eventual you do "eject" and live with tones of crap that will be deprecated very soon...

I recommend you to use CRA for learning, but not for real project or you end up fighting against its limitations and "best practies"

The only advantage is that it comes with hot reload feature, but you can do it yourself anyway or just grab it from github

Collapse
 
mehyam profile image
MehYam • Edited

I'm not crazy (CRAzy?) about CRA either, but I do use it and have found workarounds to two of the points you mention:

  • there are several npm packages that allow more customization of the CRA tsconfig.json, so you can do things like link external project folders

  • depending on your deployment platform, you can make a monorepo work. I have a server node instance that runs separate of CRA, and the CRA instance proxies unhandled requests back to it (CRA has built-in support for this). At build+deployment time, the CRA project builds to a dist folder that the main project assumes responsibility to serve, so the whole thing can run as a single server instance

Neither of these steps are very confidence inspiring, but then neither is maintaining your own custom build stack. I've done that for personal projects in the past, and it runs into a problem where you have to futz with it just infrequently enough that you forget what you did last time, and have to relearn it all over each time. Between that and the CRA boilerplate, I'm not sure what's better.

Collapse
 
butadpj profile image
Paul John Butad • Edited

In one of my Django projects (web framework using python)...

I've used REACT to handle all the frontend stuffs. Coz' Django works using Model-View-Template (MVT), so Django can use my REACT APP in its own template engine.

But, you can't rely on CRA when youre using REACT with Django. That's why I was forced to learn all of these "webpack and babel stuffs".

And gladly, it wasn't actually hard to learn. That's why I completely agree with all of the advantages that the author stated.

Collapse
 
mainendra profile image
Mainendra

Use "start": "webpack serve --mode=development --hot" for webpack cli 4 😄

Collapse
 
mainendra profile image
Mainendra

Can you add typescript too?

Collapse
 
atonchev profile image
a-tonchev

Even I need sometimes to edit webpack, to start and keep webpack really up to date with all stuff can be really pain in the as*. 'Don't use CRA...' is a little bit missleading title, better to be something like 'How to create your own CRA' :)

Collapse
 
uby profile image
Ulf Byskov

Maybe it's just bad luck, but every React project I have be working on, since CRA was made, had something which required me to either eject or use my own setup. And if that is the choice, I will go with my own setup as an ejected CRA is far from simple to deal with.

Collapse
 
annis_monadjem profile image
Annis Monadjem

Nikhil thank you very much for your excellent article!

Just a small typo in 'webpack.config.js', inside:

plugins: [
new HtmlWebPackPlugin({
template: './src/index.html',
}),
],

instead of template: './src/index.html' should be: template: './index.html'

Now, i'm able to yarn start the project!