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.
-
Make sure that you have Node.js v12 and npm v5 or above installed
-
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>
- if you want to keep my commit history:
-
Move to the appropriate directory:
cd <YOUR_PROJECT_NAME>
-
Run
npm install
in order to install dependencies -
At this point you can run
npm start
to see the React app at http://localhost:8080
-
Create your own remote repository
-
Change the remote repository's URL of the project:
git remote set-url origin <YOUR_PROJECT_REPO_URL>
-
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" }
-
Update page title in
webpack.config.js
// ... plugins: [ // ... new HtmlWebpackPlugin({ title: '<YOUR_PAGE_TITLE>', // ... }), ].filter(Boolean),
-
Update favicon
-
Update
README.md
according to your needs -
Run
npm install
in order to updatepackage.lock.json
-
Commit and push your changes:
git add . && git commit -m 'chore(settings): update project settings and readme' && git push -u origin main
-
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 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
filetouch .gitignore
-
exclude these directories and files from being added to the repository
node_modules dist coverage .vscode
- 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
-
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"));
-
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: [], };
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 loadermodule: { 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
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"]; }
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']
}
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.
-
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> `, }), ];
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/
foldertouch 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", }), ];
-
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
-
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; }
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");
Be careful when reading articles about React components hot reloading, as
react-hot-loader
is now deprecated and has to be replaced byreact-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.
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" />
-
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
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.
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.
-
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.
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" }
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
filetouch .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
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
- Creating a React App… From Scratch.
- Create React App from Scratch like a Pro
- Creating your React project from scratch without create-react-app: The Complete Guide.
- Create a React App from Scratch in 2021
- React Setup, Part 1 to 5
- Webpack Tutorial - Episode 7 - Style loaders (CSS and SCSS)
- React Refresh Webpack Plugin
- HTML Webpack Plugin
- Webpack 5 - Asset Modules
- React Architecture: How to Structure and Organize a React Application
- React Folder Structure in 5 Steps [2021]
- Jest Tutorial for Beginners: Getting Started With JavaScript Testing
- Using Jest with webpack
- React Testing Library official example
- JavaScript Testing (2 Part Series)
- Better Tests for Text Content with React Testing Library
- About Queries
- ESLint - Extending Configuration Files
- ESLint Plugin React
- Prevent missing React when using JSX
- Utiliser ESLint et Prettier pour un code de qualité
- How to setup ESLint and Prettier for your React apps
- How do I force git to use LF instead of CR+LF?
- Set Up FontAwesome with React