The most popular way of starting a React project is to use a project builder like Create React App (CRA) or Vite. But what if you didn't want to them and the conventions they enforce?

In this post, I will walk you through how you can set up a basic React project from scratch by installing and setting up the tools manually.

Sketch of the Webpack logo and the React logo

A quick overview of what we need to do

To setup a React project from scratch, we need to

The hardest thing to understand in this process is the webpack configuration file. Let's build the project in the following stages so that you can better understand the different parts of the configuration file.

  1. Setup a basic project that can bundle Javascript and JSX files into production builds
  2. Extend the project to support CSS files
  3. Extend the project to support image files
  4. Add webpack-dev-server to the project to serve the build to the browser and to automatically rebuild the project when it changes

If you are in a rush, you can finish the first section to setup a basic project and use the complete webpack configuration file from here.

Prerequisities

Make sure that you have Node.js installed. You can install the latest LTS version from here.

Setting up a basic React project

Initialize an empty npm project

First, create a new folder and initialize a npm project in it with the following command.

$ npm init -y

This will create a package.json file in the folder that describes an empty package.

Add some example code to test the project

Now we can create some example code to test whether the project works properly. At minimum, we need

You can use the snippets below to quickly create these files. In this post, we will follow the folder structure used by Create React App (CRA) but you can implement whatever folder structure you like by adjusting the paths in the webpack configuration file later.

In CRA's folder structure, the base webpage (index.html) is placed in the public folder and the Javascript files (index.js) and React components (Hello.jsx) are placed in the src folder.

public/index.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Hello World</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

src/index.js

import ReactDOM from 'react-dom/client';
import Hello from './Hello.jsx';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Hello />
);

src/Hello.jsx

const Hello = () => (
<h1>
Hello World!
</h1>
)

export default Hello;

Install React

To use React, you need two npm packages. The react package contains the core React library and all the utilities it provides (e.g. Hooks). The react-dom package contains the code for rendering React components onto a webpage. You can install both by running the command below within the project folder.

$ npm install react react-dom

Install the build tools

To bundle the JS files into a single file, we need Webpack. In addition, we also need html-webpack-plugin for webpack to extend the base webpage and babel-loader for it to use Babel. You can install them as dev-dependencies like so.

$ npm install --save-dev webpack webpack-cli html-webpack-plugin babel-loader

babel-loader needs the actual Babel package (@babel/core) in order to work. We need Babel in the project so that the JSX components can be converted into runnable JS. For this it also needs the preset-react preset. To ensure that the generated JS code is supported by the browsers that are currently in use, it is also recommended to use the preset-env preset. You can install all of these using npm like so.

$ npm install --save-dev @babel/core @babel/preset-env @babel/preset-react

Create the webpack configuration file

Webpack can be configured by placing a file named webpack.config.js in the project folder. Create webpack.config.js and place the following code inside it.

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

module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'build'),
},
plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname, 'public', 'index.html'),
})
],
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', { targets: 'defaults' }],
['@babel/preset-react', { runtime: 'automatic' }]
]
}
}
}
]
}
};

This config instructs webpack to

In addition, we also pass some additional options into babel-loader to pass to babel. Specifically, we ask it to use the @babel/preset-env and @babel/preset-react presets. @babel/preset-env handles inserting polyfills for the JS features that are not supported in the target browsers. In this case, we ask @babel/preset-env to target browsers that are used by over 0.5% of world wide users, including the last 2 versions of all major browsers, by specifying "defaults" as the target. @babel/preset-react handles converting JSX syntax into JS.

Add npm script

We can now run webpack using the npx command to generate a production build. We pass production in the mode parameter to webpack so that it enables extra error checking for the build process and minification for the outputted code.

$ npx webpack --mode production

It is more convenient to save this command as a npm script so that it can be run simply by using npm run build. Add the following statement to the scripts object inside package.json

{
...
+ "scripts": {
+ "build": "webpack --mode production"
+ }
...
}

Now when you run npm run build, a production build will be created and placed in the build folder.

$ npm run build
> react-app@1.0.0 build
> webpack --mode production

asset main.js 137 KiB [compared for emit] [minimized] (name: main) 1 related asset
asset index.html 247 bytes [emitted]
orphan modules 173 bytes [orphan] 1 module
modules by path ./node_modules/ 143 KiB
modules by path ./node_modules/react/ 7.98 KiB
modules by path ./node_modules/react/*.js 404 bytes 2 modules
modules by path ./node_modules/react/cjs/*.js 7.59 KiB 2 modules
modules by path ./node_modules/react-dom/ 131 KiB
./node_modules/react-dom/client.js 619 bytes [built] [code generated]
./node_modules/react-dom/index.js 1.33 KiB [built] [code generated]
./node_modules/react-dom/cjs/react-dom.production.min.js 129 KiB [built] [code generated]
modules by path ./node_modules/scheduler/ 4.33 KiB
./node_modules/scheduler/index.js 198 bytes [built] [code generated]
./node_modules/scheduler/cjs/scheduler.production.min.js 4.14 KiB [built] [code generated]
./src/index.js + 1 modules 406 bytes [built] [code generated]
webpack 5.91.0 compiled successfully in 2487 ms

Adding support for CSS files

The project in the previous section only works with Javascript. But it also convenient to split the CSS code and store it alongside the React components as multiple .css files. We can also configure webpack to bundle these files into a single CSS file.

Add an example CSS file

First let's add a simple CSS file with styles for the Hello component. Place it in the same folder as the component (src).

src/Hello.css

h1 {
color: red;
}

Import the example CSS file

Next we need to import this into the React component file so that webpack knows that it should process it. Do so by adding the following import to Hello.jsx.

+ import './Hello.css';

const Hello = () => (
...
)

Install the webpack plugins and loaders for handling CSS

In order for Webpack to handle CSS files, it needs css-loader to process the import statements for CSS files and mini-css-extract-plugin for extracting the CSS in those files into a single file. Install these packages with the following command.

$ npm install --save-dev mini-css-extract-plugin css-loader

Next we need to change the webpack config to use css-loader and mini-css-extract-plugin when Webpack encounters an import for a CSS file.

module.exports = {
...
plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname, 'public', 'index.html'),
}),
+ new MiniCssExtractPlugin()
]
...
module: {
rules: [
{
test: /\.(js|jsx)$/,
...
},
+ {
+ test: /\.css$/i,
+ use: [MiniCssExtractPlugin.loader, "css-loader"],
+ }
]
}
}

Now when we run npm run build, webpack will also process the imported CSS files and output them into a single CSS file.

$ npm run build
> react-app@1.0.0 build
> webpack --mode production

asset main.js 137 KiB [compared for emit] [minimized] (name: main) 1 related asset
asset index.html 286 bytes [compared for emit]
asset main.css 24 bytes [emitted] (name: main)
Entrypoint main 137 KiB = main.css 24 bytes main.js 137 KiB
orphan modules 2.96 KiB (javascript) 937 bytes (runtime) [orphan] 9 modules
cacheable modules 143 KiB (javascript) 23 bytes (css/mini-extract)
modules by path ./node_modules/ 143 KiB
modules by path ./node_modules/react/ 7.98 KiB
modules by path ./node_modules/react/*.js 404 bytes 2 modules
modules by path ./node_modules/react/cjs/*.js 7.59 KiB 2 modules
modules by path ./node_modules/react-dom/ 131 KiB
./node_modules/react-dom/client.js 619 bytes [built] [code generated]
+ 2 modules
modules by path ./node_modules/scheduler/ 4.33 KiB
./node_modules/scheduler/index.js 198 bytes [built] [code generated]
./node_modules/scheduler/cjs/scheduler.production.min.js 4.14 KiB [built] [code generated]
modules by path ./src/ 402 bytes (javascript) 23 bytes (css/mini-extract)
./src/index.js + 1 modules 402 bytes [built] [code generated]
css ./node_modules/css-loader/dist/cjs.js!./src/Hello.css 23 bytes [built] [code generated]
webpack 5.91.0 compiled successfully in 3349 ms

Adding support for image files

We can also set up Webpack to handle image files so that we don't have to manually manage the URLs for the images used in the React components or CSS files.

Add an example icon component

To test this out, let's create an Icon component that uses an image. If you don't have an image handy, you can use this svg image.

src/Icon.jsx

import reactIcon from './react.svg';

const Icon = () => (
<img src={reactIcon} alt="React Logo" width={64} height={64} />
);

export default Icon;

Then extend the existing Hello component to use this Icon component.

src/Hello.jsx

import Icon from './Icon.jsx';

const Hello = () => (
<div>
<Icon />
<h1>Hello World!</h1>
</div>
)

export default Hello;

Extend the Webpack config to handle imported images files

Webpack 5 has built in support for handling imported asset files such as images. So we only need to add a rule telling Webpack to treat image files as asset/resource.

module.exports = {
...
module: {
rules: [
...,
+ {
+ test: /\.(png|jpe?g|svg)$/i,
+ type: 'asset/resource'
+ }
]
}
}

Now when we run npm run build, Webpack will also process the imported image files and output them to the output folder. It will then adjust the string that is returned from the import statement in the Icon component to point to the path were it outputted the image file.

$ npm run build

> react-app@1.0.0 build
> webpack --mode production

assets by status 50.5 KiB [cached] 2 assets
assets by path . 138 KiB
asset stuff.js 138 KiB [compared for emit] [minimized] (name: main) 1 related asset
asset index.html 287 bytes [compared for emit]
asset main.css 77 bytes [emitted] (name: main)
Entrypoint main 138 KiB (7.41 KiB) = main.css 77 bytes stuff.js 138 KiB 1 auxiliary asset
orphan modules 4.19 KiB (javascript) 43.1 KiB (asset) 1.06 KiB (runtime) [orphan] 14 modules
runtime modules 1.15 KiB 2 modules
cacheable modules 144 KiB (javascript) 7.41 KiB (asset) 155 bytes (css/mini-extract)
modules by path ./node_modules/ 143 KiB
modules by path ./node_modules/react/ 7.98 KiB 4 modules
modules by path ./node_modules/react-dom/ 131 KiB
./node_modules/react-dom/client.js 619 bytes [built] [code generated]
+ 2 modules
modules by path ./node_modules/scheduler/ 4.33 KiB
./node_modules/scheduler/index.js 198 bytes [built] [code generated]
./node_modules/scheduler/cjs/scheduler.production.min.js 4.14 KiB [built] [code generated]
modules by path ./src/ 900 bytes (javascript) 7.41 KiB (asset) 155 bytes (css/mini-extract)
./src/index.js + 3 modules 858 bytes [built] [code generated]
./src/react.svg 42 bytes (javascript) 7.41 KiB (asset) [built] [code generated]
css ./node_modules/css-loader/dist/cjs.js!./src/Hello.css 155 bytes [built] [code generated]
webpack 5.91.0 compiled successfully in 2392 ms

Setting up webpack-dev-server

The React project we have right now is tedious to use since we need to manually run the build command every time we make changes to code. We also need to manually open the generated files to view them.

We can solve this problem by using webpack-dev-server. It is a Webpack module that sets up a HTTP server that automatically serves the files generated by Webpack so that they can be viewed in a browser. It also watches the source files and automatically rebuilds them when a change is detected. So we can change the code and immediately see the changes in the browser.

Installing and using webpack-dev-server

You can install webpack-dev-server by running the following command.

$ npm install --save-dev webpack-dev-server 

You can now start the dev server by using npx webpack serve --mode development. We set the mode parameter to development in this case since we want Webpack to generate unminified code that is easier to debug. We can also add this to package.json as a script so that we can run the more memorable npm start command to start the dev server.

{
...
+ "scripts": {
+ "start": "webpack serve --mode development"
+ }
...
}

Running npm start will now start the dev server.

$ npm start  

> react-app@1.0.0 start
> webpack serve --mode development

<i> [webpack-dev-server] Project is running at:
<i> [webpack-dev-server] Loopback: http://localhost:8080/
<i> [webpack-dev-server] On Your Network (IPv4): http://192.168.2.119:8080/
<i> [webpack-dev-server] On Your Network (IPv6): http://[fe80::1]:8080/
<i> [webpack-dev-server] Content not from webpack is served from '/Users/bash/temp/webpack-scratch/public' directory
asset main.js 1.43 MiB [emitted] (name: main)
asset index.html 315 bytes [emitted]
asset main.css 236 bytes [emitted] (name: main)
Entrypoint main 1.43 MiB = main.css 236 bytes main.js 1.43 MiB
runtime modules 44 KiB 23 modules
orphan modules 2.75 KiB [orphan] 3 modules
modules by path ./node_modules/ 1.3 MiB 37 modules
modules by path ./src/ 725 bytes (javascript) 23 bytes (css/mini-extract)
modules by path ./src/*.css 323 bytes (javascript) 23 bytes (css/mini-extract)
./src/Hello.css 323 bytes [built] [code generated]
css ./node_modules/css-loader/dist/cjs.js!./src/Hello.css 23 bytes [built] [code generated]
./src/index.js 233 bytes [built] [code generated]
./src/Hello.jsx 169 bytes [built] [code generated]
webpack 5.91.0 compiled successfully in 1047 ms

Setting up style-loader

We currently use mini-css-extract-plugin to combine the imported CSS files into a single CSS file. This can be slow during development. To resolve this, the creators of css-loader [recommend] (https://github.com/webpack-contrib/css-loader?tab=readme-ov-file#recommend) style-loader. It places style tags in the base webpage that link directly to the imported CSS files and is faster than mini-css-extract-plugin.

The tricky thing is that style-loader should not be used in production. We should only use it when running Webpack locally for development. We can set up this behavior by switching to the function form of the Webpack configuration so that we can check the mode parameter that is passed into Webpack.

Install style-loader using npm.

$ npm install --save-dev style-loader

Then set Webpack to use it only during development by changing webpack.config.js like so

- module.exports = {
+ module.exports = (env, argv) => {
+ const devMode = argv.mode !== 'production';

+ return {
entry: './src/index.js',
...
module: {
rules: [
...
{
test: /\.css$/i,
- use: [MiniCssExtractPlugin.loader, "css-loader"],
+ use: [
+ devMode ? "style-loader" : MiniCssExtractPlugin.loader,
+ "css-loader"
+ ],
}
]
}
}
}

Conclusion

If you made it this far, congratulations! You now have a React project that you can use to build a React web app. In case you missed any of the steps above, you can find the final webpack.config.js here.

You can now add extra Webpack plugins and loader to enable extra features. For example, you can use sass-loader for adding SASS support and CssMinimizerWebpackPlugin to minimize the generated CSS.

Prabashwara Seneviratne (bash)

Written by

Prabashwara Seneviratne (bash)

Lead frontend developer