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;
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
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
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
index.js
(entry)main.js
in a folder named build
(output)main.js
file in the index.html
file and place that in the build (HtmlWebpackPlugin).js
and .jsx
files it finds imported (module, rules)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.
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
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.
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;
}
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 = () => (
...
)
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
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.
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;
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
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.
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
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"
+ ],
}
]
}
}
}
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.