Skip to content

A cutting-edge React boilerplate with his step-by-step guide

Notifications You must be signed in to change notification settings

lamai6/react-app-starter

Repository files navigation

Start your next React app with best practices and best tools for React

Why this project ?

As a backend developer, I did not have the opportunity to work on frontend projects at work. Frontend development has always interested me, and now I aspire to master React framework. And the best way to start learning for me is to understand how frontend tools work together under the hood. Rather than using create-react-app, I had better create my own version from scratch. That's why I created this boilerplate, along with the following brief guide that I fed throughout the development of this project.

If you are interested to see how I created this React boilerplate step by step, you can go to the first section of this guide. If not, read only the Quick start section.

Quick start

  1. Make sure that you have Node.js v12 and npm v5 or above installed

  2. Clone this repo using git clone:

    • if you want to keep my commit history: git clone git@github.com:lamai6/react-app-starter.git <YOUR_PROJECT_NAME>
    • if you want to merge all my commits into a single commit: git clone --depth=1 git@github.com:lamai6/react-app-starter.git <YOUR_PROJECT_NAME>
  3. Move to the appropriate directory: cd <YOUR_PROJECT_NAME>

  4. Run npm install in order to install dependencies

  5. At this point you can run npm start to see the React app at http://localhost:8080

Optional

  1. Create your own remote repository

  2. Change the remote repository's URL of the project: git remote set-url origin <YOUR_PROJECT_REPO_URL>

  3. Update these package.json properties

    {
      "name": "<YOUR_PROJECT_NAME>",
      "description": "<YOUR_PROJECT_DESCRIPTION>",
      "repository": {
        "url": "git+<YOUR_PROJECT_REPO_URL>"
      },
      "bugs": {
        "url": "<YOUR_PROJECT_REPO_URL>/issues"
      },
      "homepage": "<YOUR_PROJECT_REPO_URL>#readme"
    }
  4. Update page title in webpack.config.js

    // ...
    plugins: [
      // ...
      new HtmlWebpackPlugin({
        title: '<YOUR_PAGE_TITLE>',
        // ...
      }),
    ].filter(Boolean),
  5. Update favicon

  6. Update README.md according to your needs

  7. Run npm install in order to update package.lock.json

  8. Commit and push your changes: git add . && git commit -m 'chore(settings): update project settings and readme' && git push -u origin main

Initialize git repository

  • create a new GitHub repository

  • initialize the local repository

    echo "New Project" >> README.md
    git init
    git add README.md
    git commit -m "first commit"
    git branch -M main
    git remote add origin git@github.com:username/repo_name.git
    git push -u origin main

Install dependencies

  • install all the dependencies needed for our React app

    npm init -y
    # React
    npm install --save react react-dom
    # React Refresh plugins
    npm install --save-dev @pmmmwh/react-refresh-webpack-plugin react-refresh
    # webpack
    npm install --save-dev webpack webpack-cli webpack-dev-server cross-env
    # webpack loaders and plugins
    npm install --save-dev babel-loader style-loader css-loader sass-loader node-sass html-webpack-plugin
    # Babel compiler and presets
    npm install --save-dev @babel/core @babel/cli @babel/preset-react @babel/preset-env
    # Testing
    npm install --save-dev jest @testing-library/react @testing-library/jest-dom
    # Linter
    npx install-peerdeps --dev eslint-config-airbnb
    npm install --save-dev @babel/eslint-parser eslint-plugin-jest eslint-import-resolver-webpack
    # Formatter
    npm install --save-dev --save-exact prettier
  • create .gitignore file

    touch .gitignore
  • exclude these directories and files from being added to the repository

    node_modules
    dist
    coverage
    .vscode
    

Structure the project

  • create project structure
    mkdir src && touch src/index.jsx
    # Assets
    mkdir src/assets && mkdir src/assets/images && mkdir src/assets/fonts
    # Components
    mkdir src/components && mkdir src/components/App && touch src/components/App/App.jsx && touch src/components/App/App.styles.scss
    # Services
    mkdir src/services

Create a demo app

  • add SCSS styles in src/components/App/App.styles.scss

    $text-color: white;
    $image_url: url("../../assets/images/odaiba-night.jpg");
    $font_url: url("../../assets/fonts/roboto-regular.ttf");
    
    @font-face {
      font-family: "Roboto";
      src: $font_url format("truetype");
      font-weight: 400;
      font-style: normal;
    }
    
    body {
      background-image: $image_url;
      font-family: "Roboto", sans-serif;
    }
    
    #counter {
      color: $text-color;
    
      h3 {
        text-align: center;
      }
    
      div {
        display: flex;
        justify-content: center;
    
        button {
          margin: 0 1em;
        }
      }
    }
  • add a React component in src/components/App/App.jsx

    import { Component } from "react";
    import "./App.styles.scss";
    
    class App extends Component {
      constructor(props) {
        super(props);
        this.state = {
          count: 0,
        };
        this.increment = this.increment.bind(this);
        this.decrement = this.decrement.bind(this);
        this.reset = this.reset.bind(this);
      }
    
      increment() {
        this.setState((state) => ({
          count: state.count + 1,
        }));
      }
    
      decrement() {
        this.setState(
          (state) => (state.count > 0 && { count: state.count - 1 }) || state
        );
      }
    
      reset() {
        this.setState(() => ({ count: 0 }));
      }
    
      render() {
        const { count } = this.state;
        return (
          <div id="counter">
            <h3>
              <span>Counter: </span>
              {count}
            </h3>
            <div>
              <button type="button" onClick={this.increment}>
                Increment
              </button>
              <button type="button" onClick={this.decrement}>
                Decrement
              </button>
              <button type="button" onClick={this.reset}>
                Reset
              </button>
            </div>
          </div>
        );
      }
    }
    
    export default App;
  • edit main .js file in src/index.jsx

    import { render } from "react-dom";
    import App from "./components/App/App";
    
    render(<App />, document.getElementById("app"));

Configure Webpack

Webpack initial configuration

  • create Webpack config file

    touch webpack.config.js
  • add Webpack config to webpack.config.js

    const path = require("path");
    const isDevelopment = process.env.NODE_ENV !== "production";
    
    module.exports = {
      mode: isDevelopment ? "development" : "production",
      entry: "./src/app.js",
      output: {
        filename: "app.bundle.js",
        path: path.resolve(__dirname, "dist"),
        clean: true,
      },
      module: {
        rules: [],
      },
      plugins: [],
    };

Handle Javascript files with Babel

Outside Webpack (preferred method)

We could configure Babel inside webpack, but this is not the best option for our app as we'll have other tools that need to use Babel, as Jest (a testing framework). Hence, we can add a configuration file at the root of the project that will be shared by webpack and others.

  • create Babel config file

    touch babel.config.js
  • add Babel config to .babel.config.js

    module.exports = (api) => {
      api.cache.using(() => process.env.NODE_ENV);
      return {
        presets: [
          '@babel/preset-env',
          // JSX transpiles <App /> to React.createElement(...), so we ask runtime to auto import React
          [
            '@babel/preset-react',
            { development: !api.env('production'), runtime: 'automatic' },
          ],
        ],
        plugins: [
          '@babel/plugin-transform-runtime', // for ES7 features like async/await
          !(api.env('production') || api.env('test')) && 'react-refresh/babel',
        ].filter(Boolean),
      };
    };
  • add a rule for .js files in ./webpack.config.js and provide the Babel loader

    module: {
      rules: [
        {
          test: /\.(js|jsx)$/,
          exclude: /node_modules/,
          loader: "babel-loader",
        },
      ];
    }
  • tell webpack to resolve .jsx files with resolve object in webpack.config.js

    resolve: {
      extensions: [".js", ".jsx"];
    }

If you want to compile a .js(x) file outside of Webpack using Babel, use the Babel CLI

./node_modules/.bin/babel src/components/App/App.jsx -o dist/App.js

Inside Webpack (alternative method)

If Babel will be used only by webpack, you can configure Babel directly inside Webpack configuration (./webpack.config.js) by providing loader options to the .js files' rule, instead of creating a ./babel.config.js config file.

  • add babel loader configuration to the rule in webpack.config.js

    {
        test: /\.(js|jsx)$/i,
        exclude: /node_modules/,
        use: [
            {
                loader: require.resolve('babel-loader'),
                options: {
                    presets: ["@babel/preset-env", "@babel/preset-react"],
                    plugins: ["@babel/plugin-transform-runtime", isDevelopment && require.resolve('react-refresh/babel')].filter(Boolean)
                }
            }
        ]
    }
  • tell webpack to resolve .jsx files with resolve object in webpack.config.js

    resolve: {
      extensions: [".js", ".jsx"];
    }

Handle (S)CSS files

The sass-loader compiles scss files into css files, css-loader resolves url() method and @import rule and styles-loader inject css into the DOM.

{
    test: /\.s?css$/i,
    use: ['style-loader', 'css-loader', 'sass-loader']
}

Create HTML template

We need an HTML page that will be notably used by React to inject the app. To create it, we will use html-webpack-plugin and create a custom template.

Preferred method

  • add a favicon in the src/assets/images folder

  • add plugins object in webpack configuration (webpack.config.js)

    // put this line with other variable declarations
    const HtmlWebpackPlugin = require("html-webpack-plugin");
    
    plugins: [
      new HtmlWebpackPlugin({
        title: "React App",
        inject: false,
        favicon: './src/assets/images/favicon.png',
        templateContent: ({ htmlWebpackPlugin }) => `
                <html>
                    <head>
                        <title>${htmlWebpackPlugin.options.title}</title>
                        ${htmlWebpackPlugin.tags.headTags}
                    </head>
                    <body>
                        <div id='app'></div>
                        ${htmlWebpackPlugin.tags.bodyTags}
                    </body>
                </html>
            `,
      }),
    ];

Alternative method

You can create a template in a separate HTML file in you need webpack to watch for template file changes. For a React app, this is not really useful.

  • create the template file in src/ folder

    touch src/template.html
  • add a favicon in the src/assets/images folder

  • insert HTML template (for webpack) in src/template.html

    <!DOCTYPE html>
    <html>
      <head>
        <meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
        <title>React App</title>
      </head>
      <body>
        <div id="app"></div>
      </body>
    </html>
  • add plugins object in webpack configuration (webpack.config.js)

    plugins: [
      new HtmlWebpackPlugin({
        title: "React App",
        favicon: './src/assets/images/favicon.png',
        template: "src/template.html",
      }),
    ];

Handle assets

Images

  • add an image in the src/assets/images folder

  • add new rule in webpack module object (webpack.config.js)

    {
        test: /\.(png|svg|jpg|jpeg|gif)$/i,
        type: 'asset/resource',
        generator: {
            filename: 'images/[hash][ext][query]'
        }
    }
  • add, for instance, a background image to body in styles.scss

    $image_url: url("assets/images/odaiba-night.jpg");
    
    body {
      background-image: $image_url;
    }

Webpack will move our image in dist/images folder and update the background-image property with the new location of the image

Fonts

  • add a font in the src/assets/fonts folder

  • add new rule in webpack module object (webpack.config.js)

    {
        test: /\.(woff|woff2|eot|ttf|otf)$/i,
        type: 'asset/resource',
        generator: {
            filename: 'fonts/[hash][ext][query]'
        }
    }
  • add, for instance, Roboto font to body in styles.scss

    $font_url: url("assets/fonts/roboto-regular.ttf");
    
    @font-face {
      font-family: "Roboto";
      src: $font_url format("truetype");
      font-weight: 400;
      font-style: normal;
    }
    
    body {
      font-family: "Roboto", sans-serif;
    }

Create aliases

Facilitate imports in .js(x) and (s)css files by adding aliases to some recurrent directories as src/assets or src/components

resolve: {
    extensions: ['.js', '.jsx'],
    alias: {
        '@': path.resolve(__dirname, 'src'),
        '@assets': path.resolve(__dirname, 'src/assets'),
        '@components': path.resolve(__dirname, 'src/components')
    }
}

You can now update imports:

  • in src/index.jsx

    import App from "@components/App/App";
  • in src/components/App/App.styles.scss

    $image_url: url("@images/odaiba-night.jpg");
    $font_url: url("@fonts/roboto-regular.ttf");

Webpack Dev Server & HMR

Be careful when reading articles about React components hot reloading, as react-hot-loader is now deprecated and has to be replaced by react-refresh, developed by the React team. To connect React Fast Refresh with webpack, a plugin has been developed.

  • add devServer object in webpack configuration (webpack.config.js)

    devServer: {
        static: './dist',
        open: true,
        hot: true
    }
  • add react-refresh plugin to Babel loader configuration (webpack.config.js)

    options: {
        presets: ["@babel/preset-env", "@babel/preset-react"],
        plugins: [isDevelopment && require.resolve('react-refresh/babel')].filter(Boolean)
    }
  • connect webpack and react-refresh by adding our connector plugin to plugins object (webpack.config.js)

    // put this line with other variable declarations
    const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
    
    // add the connector plugin to the plugins property of webpack config
    plugins: [
        isDevelopment && new ReactRefreshWebpackPlugin(),
        new HtmlWebpackPlugin({
            title: 'React App',
            inject: false,
            favicon: './src/assets/images/favicon.png',
            templateContent: ({htmlWebpackPlugin}) => `
                <html>
                    <head>
                        <title>${htmlWebpackPlugin.options.title}</title>
                        ${htmlWebpackPlugin.tags.headTags}
                    </head>
                    <body>
                        <div id='app'></div>
                        ${htmlWebpackPlugin.tags.bodyTags}
                    </body>
                </html>
            `
        })
    ].filter(Boolean),
  • add 2 scripts in package.json: one to run the webpack server, the other to build manually ./dist folder with all assets and mode of your choice (development / production)

    "scripts": {
      "test": "echo \"Error: no test specified\" && exit 1",
      "build": "cross-env NODE_ENV=production webpack",
      "start": "webpack serve"
    }
  • execute the start script with npm, webpack will launch the app on your browser after building assets

    npm run start

Furthermore, don't add the output.clean option in webpack configuration, it will prevent React components from hot reloading. For more information, take a look at the issue I opened on the connector plugin repository.

Use FontAwesome icons

There are a few ways to add FontAwesome to a project. The following is the best way as only the icons used will be included in the build.

  • add FontAwesome svg core

    npm i --save @fortawesome/fontawesome-svg-core
  • install icon styles you need

    npm i --save @fortawesome/free-solid-svg-icons
    # npm i --save @fortawesome/free-brands-svg-icons
    # npm i --save @fortawesome/free-regular-svg-icons
  • install FontAwesome React component

    npm i --save @fortawesome/react-fontawesome@latest
  • install Babel macros

    npm install --save-dev babel-plugin-macros
  • add the plugin to Babel config file

    plugins: [
      '@babel/plugin-transform-runtime',
      'macros',
      !(api.env('production') || api.env('test')) && 'react-refresh/babel',
    ].filter(Boolean),
  • create babel-plugin-macros.config.js and add the fontawesome-svg-core settings

    module.exports = {
      "fontawesome-svg-core": {
        license: "free",
      },
    };
  • add icons to react component

    // top of the file
    import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
    import { solid } from '@fortawesome/fontawesome-svg-core/import.macro'
    
    // use icons
    <FontAwesomeIcon icon={solid('plus')} size="sm" />
    <FontAwesomeIcon icon={solid('minus')} size="sm" />

Test your app with Jest

  • create Jest file configuration

    touch ./jest.config.js
  • insert this configuration in this file

    module.exports = {
      moduleNameMapper: {
        "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
          "<rootDir>/src/__mocks__/fileMock.js",
        "\\.(scss|css|less)$": "<rootDir>/src/__mocks__/styleMock.js",
      },
      testEnvironment: "jest-environment-jsdom",
    };

Jest uses Babel to transpile JSX, but Jest cannot resolve stylesheets or assets imports. This is not a problem as we don't need them to test our React components, hence we have to mock them.

  • create 2 mock files, one for stylesheets, the other for assets

    mkdir src/__mocks__ && touch src/__mocks__/fileMock.js && touch src/__mocks__/styleMock.js
    • in fileMock.js, add:

      module.exports = "test-file-stub";
    • in styleMock.js, add:

      module.exports = {};
  • update test script in package.json to call jest library

    "scripts": {
      "test": "jest --coverage",
      "build": "cross-env NODE_ENV=production webpack",
      "start": "webpack serve"
    }
  • create a test file for App component

    touch ./src/components/App/App.test.jsx
  • add some tests to this test file

    import { render, screen, fireEvent } from "@testing-library/react";
    import "@testing-library/jest-dom";
    import App from "./App";
    
    describe("Count incrementation", () => {
      it("should increment counter when clicking on increment button", () => {
        render(<App />);
        const title = screen.getByRole("heading", { name: /counter/i });
        const incButton = screen.getByRole("button", { name: /increment/i });
    
        expect(title).toHaveTextContent("Counter: 0");
        fireEvent.click(incButton);
        expect(title).toHaveTextContent("Counter: 1");
      });
    });
    
    describe("Count decrementation", () => {
      it("should decrement counter when clicking on decrement button", () => {
        render(<App />);
        const title = screen.getByRole("heading", { name: /counter/i });
        const incButton = screen.getByRole("button", { name: /increment/i });
        const decButton = screen.getByRole("button", { name: /decrement/i });
    
        fireEvent.click(incButton);
        expect(title).toHaveTextContent("Counter: 1");
        fireEvent.click(decButton);
        expect(title).toHaveTextContent("Counter: 0");
      });
    
      it("should not decrement counter under 0 when clicking on decrement button", () => {
        render(<App />);
        const title = screen.getByRole("heading", { name: /counter/i });
        const decButton = screen.getByRole("button", { name: /decrement/i });
    
        expect(title).toHaveTextContent("Counter: 0");
        fireEvent.click(decButton);
        expect(title).toHaveTextContent("Counter: 0");
      });
    });
    
    describe("Count reset", () => {
      it("should reset counter when clicking on reset button", () => {
        render(<App />);
        const title = screen.getByRole("heading", { name: /counter/i });
        const incButton = screen.getByRole("button", { name: /increment/i });
        const resButton = screen.getByRole("button", { name: /reset/i });
    
        fireEvent.click(incButton);
        fireEvent.click(incButton);
        expect(title).toHaveTextContent("Counter: 2");
        fireEvent.click(resButton);
        expect(title).toHaveTextContent("Counter: 0");
      });
    });
  • run the tests

    npm test

Lint your code with ESLint

We choose Airbnb rules preset for our project

  • create ESLint file configuration

    touch .eslintrc.js
  • insert this configuration in this new file

    const jest = require("jest/package.json");
    
    module.exports = {
      extends: ["airbnb", "plugin:react/jsx-runtime", "plugin:jest/recommended"],
      parser: "@babel/eslint-parser",
      plugins: ["jest"],
      rules: {
        "comma-dangle": [
          "error",
          {
            arrays: "only-multiline",
            objects: "always-multiline",
            imports: "only-multiline",
            exports: "only-multiline",
            functions: "only-multiline",
          },
        ],
      },
      settings: {
        react: {
          version: "detect",
        },
        jest: {
          version: jest.version,
        },
        "import/resolver": "webpack",
      },
      overrides: [
        {
          files: ["**/*.js", "**/*.jsx"],
        },
      ],
      env: {
        browser: true,
        node: true,
      },
      ignorePatterns: [
        "node_modules",
        "dist",
        "coverage",
        ".vscode",
        "/src/assets/",
        "*.md",
      ],
    };
  • add lint script in package.json to call ESLint

    "scripts": {
      "test": "jest --coverage",
      "build": "cross-env NODE_ENV=production webpack",
      "start": "webpack serve",
      "lint": "eslint src/**/*.js src/**/*.jsx",
      "lint-debug": "npm run lint -- --debug",
    }

You can download the ESLint plugin of your favorite IDE here, so that you don't have to run the lint script every time.

Format your code with Prettier

No integration with ESLint wanted. Why ?

While many developers install the eslint-plugin-prettier plugin, I decided not to use it. Why ? Because this plugin turns off all ESLint rules related to code formatting (with eslint-config-prettier package) to avoid conflicts between ESLint and Prettier, and adds its own formatting rules to ESLint, which leads us to lose all Airbnb formatting rules.

So, I added Prettier to the project, and configured it myself (prettier.config.js) in a way that it adapts to ESLint formatting rules, even if Prettier's options are a bit limited.

Configuration

  • create Prettier file configuration

    touch prettier.config.js
  • add this configuration in this file

    module.exports = {
      trailingComma: "es5",
      tabWidth: 2,
      semi: true,
      singleQuote: true,
      bracketSpacing: true,
      endOfLine: "lf",
      arrowParens: "always",
    };
  • create Prettier ignoring file

    touch .prettierignore
  • add theses patterns to the file

    node_modules
    dist
    coverage
    .vscode
    /src/assets/
    *.md
    
  • create .editorconfig file to ensure lines end with a line feed (\n)

    touch .editorconfig
  • add these rules to the file

    root = true
    
    [*]
    charset = utf-8
    end_of_line = lf
    indent_style = space
    indent_size = 2
    tab_width = 2
    insert_final_newline = true
    trim_trailing_whitespace = true
    
    [*.{md,markdown}]
    insert_final_newline = false
    trim_trailing_whitespace = false
  • add format script in package.json to call Prettier

    "scripts": {
      "test": "jest --coverage",
      "build": "cross-env NODE_ENV=production webpack",
      "start": "webpack serve",
      "lint": "eslint src/**/*.js src/**/*.jsx",
      "lint-debug": "npm run lint -- --debug",
      "format": "prettier --write"
    }

You can download the Prettier plugin of your favorite IDE here, so that you can format files on save.

Visual Studio Code issues on Windows

On VSCode, the linebreaks are carriage returns (CR) followed by a line feed (LF) by default. But our files' linebreaks have to be only a line feed according to this ESLint rule.

Having .editorconfig file and Prettier configuration is not enough to create new files with LF linebreaks by default, due to a still unsolved issue.

If you have many files that have CRLF linebreaks, there is currently no way to change the linebreak's type at once, you have to do it for each file.

Moreover, installing Prettier plugin for VSCode is not enough, you have to make Prettier the default formatter and activate manually the format on save feature (see below).

To fix these issues ourselves:

  • open your settings.json file (location: %APPDATA%\Code\User\settings.json) and add:

    {
      // ...
      "editor.defaultFormatter": "esbenp.prettier-vscode",
      "editor.formatOnSave": true,
      "files.eol": "\n"
    }

Conflict between git and ESLint about end of line on Windows

When you commit a file, git converts automatically CRLF linebreaks into LF linebreaks, which is a good thing. It also converts LF linebreaks into CRLF linebreaks on Windows system when the file is checked out.

However, the conversion into CRLF violates the ESLint linebreak-style rule, which leads to many errors when running npm run lint.

To only prevent git from converting LF into CRLF (solution scoped to project):

  • create .gitattributes file

    touch .gitattributes
  • add this pattern to this file

    * text=auto eol=lf

If you also want to prevent globally this conversion (on all your current and future projects):

  • run this command

    git config --global core.autocrlf false

Deploy your app with GitHub Pages

We have to build the app and push the content of the build directory to a new branch named gh-pages. GitHub will then host the content of this branch. We will automate this process using gh-pages module.

  • install gh-pages package

    npm install --save-dev gh-pages
  • update package.json file

    {
      // ...
      "homepage": "https://<YOUR_GITHUB_USERNAME>.github.io/<YOUR_PROJECT_NAME>",
      "scripts": {
        // ...
        "predeploy": "npm run build",
        "deploy": "gh-pages -d dist",
      },
    }
  • build and deploy the app

    npm run deploy

Some resources that helped me

About

A cutting-edge React boilerplate with his step-by-step guide

Resources

Stars

Watchers

Forks