Artwork: https://code-art.pictures/
Code on artwork: React JS
Why bother if there is create-react-app?
Good question! In fact, if you're happy with create-react-app
, feel free to use it 🙂 However, if you're curious about how everything works together, let's combine all the parts ourselves!
The full code
The complete code for this example is available here. You can simply copy-paste and use it, or follow along with this blog post.
Note on keeping it current
Dear reader and fellow developer, I strive to keep this post up to date. However, if you come across any inaccuracies or outdated information, please feel free to leave a comment. Thank you!
Structure of the project we are going to create
/hello-react
/dist
index.html
main.da363aed60cf1cf68088.css
main.db8733ab04253d079b1c.js
main.db8733ab04253d079b1c.js.LICENSE.txt
/src
index.css
index.tsx
index.html
package.json
tsconfig.json
webpack.config.mjs
1. Install Node.js and npm
The steps for installing Node.js depend on your operating system. Visit the download page and follow the provided instructions.
npm doesn't require separate installation because it comes bundled with Node.js. To verify that everything is installed correctly on your system, refer to these instructions.
Sidenote: Node.js and npm are not the only options. There’s Deno, an alternative to Node.js, and Yarn, an alternative to npm. If you're unsure, I recommend sticking with Node.js and npm for now.
2. Create the project
Create the project root directory, hello-react
, and run the npm init
wizard from inside it:
mkdir hello-react
cd hello-react
npm init
The wizard will guide you through creating an empty project by asking questions one by one. To automatically accept all default answers, you can append the -y
parameter to the npm init
command.
Once the wizard finishes, it creates the following file:
package.json
{
"name": "hello-react",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
Not much, but... that's already a valid Node.js project! 🎊
3. Install TypeScript
From the project root directory, run the following command:
npm i --save-dev typescript
4. Create tsconfig.json
This file contains the TypeScript configuration for your project. Create a tsconfig.json
file in the project root directory and insert the following content:
tsconfig.json
{
"compilerOptions": {
"esModuleInterop": true,
"jsx": "react-jsx",
"module": "esnext",
"moduleResolution": "bundler",
"lib": [
"dom",
"esnext"
],
"strict": true,
"sourceMap": true,
"target": "esnext",
},
"exclude": [
"node_modules"
]
}
What do these options mean? Let’s take a look!
compilerOptions
-
esModuleInterop
: Fixes default and namespace imports from CommonJS to TypeScript. This is generally required for compatibility. -
jsx
: Specifies how TypeScript should transpile JSX files (e.g.,react-jsx
for React). -
module
: Determines how TypeScript transpiles ES6 imports and exports. Setting it toesnext
leaves them unchanged, which I recommend so webpack can handle this instead. -
moduleResolution
: Specifies how TypeScript resolves modules, which depends on the target runtime. Since our app is bundled with webpack,bundler
is the best choice. Earlier versions used thenode
option, which would also work for our example. -
lib
: Specifies which libraries exist in your target environment, so TypeScript implicitly includes their types. Note: TypeScript cannot verify if these libraries are actually available at runtime — this is your promise. More details later. -
strict
: Enables all strict type-checking options in TypeScript for maximum type safety. -
sourceMap
: Enables TypeScript to emit source maps. We’ll configure webpack to exclude these in production builds. -
target
: Configures the target ECMAScript version, which depends on your users' environments. More details on this later.
exclude
-
exclude
: Excludes libraries from type-checking and transpilation. However, your code is still checked against the type definitions provided by these libraries.
For the full tsconfig.json
reference, check out the official documentation.
5. Install webpack, plugins and loaders
From the project root directory, run the following command. It’s a long one, so ensure you’ve scrolled enough and copied the entire line!
npm i --save-dev webpack webpack-cli webpack-dev-server css-loader html-webpack-plugin mini-css-extract-plugin esbuild-loader
6. Create webpack.config.mjs
Create a file named webpack.config.mjs
in the project root directory and insert the following content:
webpack.config.mjs
import HtmlWebpackPlugin from 'html-webpack-plugin'
import MiniCssExtractPlugin from 'mini-css-extract-plugin'
const prod = process.env.NODE_ENV === 'production'
export default {
mode: prod ? 'production' : 'development',
devtool: prod ? undefined : 'source-map',
entry: './src/index.tsx',
module: {
rules: [
{
test: /\.(ts|tsx)$/,
exclude: /node_modules/,
loader: 'esbuild-loader',
options: {
target: 'esnext',
jsx: 'automatic',
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.json'],
},
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
]
},
output: {
filename: '[name].[contenthash].js',
path: import.meta.dirname + '/dist/',
},
plugins: [
new HtmlWebpackPlugin({
template: 'index.html',
}),
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
}),
],
}
A lot is going on here! Webpack configuration is arguably the most complex part of the entire setup. Let’s break it down step by step:
-
Setting a
NODE_ENV
variable: This is the typical way to differentiate between development and production modes. Later, you'll see how to set it in your script. -
HtmlWebpackPlugin
: Generates adist/index.html
file from a templateindex.html
, which we’ll create shortly. -
MiniCssExtractPlugin
: Extracts styles into a separate file. Without this, styles would remain inline inindex.html
. -
mode
: Specifies whether your build is for development or production. In production mode, Webpack automatically minifies the bundle. -
devtool
: Configures source maps for easier debugging. -
entry
: Defines the module to execute first after your app loads in the client. This serves as the bootstrap to launch your application. -
module.rules
: Describes how different types of files are loaded (imported) into the bundle.-
test: /\.(ts|tsx)$/
: Processes TypeScript files usingesbuild-loader
. See an example here. -
test: /\.css$/
: Processes CSS files.
-
-
output
:-
filename
: Sets the filename to include a content hash. This technique is known as “cache busting”. -
path
: Specifies the target directory for compiled files.
-
-
plugins
: Lists all plugins and their settings.
6.1. Alternative: webpack config as a CJS file
We defined the webpack configuration in an ESM file. This is possible since version 4.5 and should work on recent node
versions. However, if it doesn't work for you, you can switch your configuration to CJS.
webpack.config.js
First, change the file extension from .mjs
to .js
. Then, update the content as follows:
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const prod = process.env.NODE_ENV === 'production'
module.exports = {
// (no changes)
output: {
// (no changes)
// The only change:
// replace import.meta.dirname with __dirname
path: __dirname + '/dist/',
},
// (no changes)
}
6.2. Alternative: use ts-loader
instead of esbuild-loader
We used esbuild-loader to load TypeScript files. As of 2025, it is likely the fastest TypeScript loader for webpack.
However, according to the webpack documentation, ts-loader
is the default recommended TypeScript loader. Unlike esbuild-loader
, which is relatively new, ts-loader
is older and more mature. If you have a more complex setup, ts-loader
is more likely to offer reliable support.
Changing to ts-loader
npm uninstall esbuild-loader
npm i -D ts-loader
webpack.config.mjs
// ...
export default {
// ...
module: {
rules: [
// {
// test: /\.(ts|tsx)$/,
// exclude: /node_modules/,
// loader: 'esbuild-loader',
// options: {
// target: 'esnext',
// jsx: 'automatic',
// },
// resolve: {
// extensions: ['.ts', '.tsx', '.js', '.json']
// },
// },
{
test: /\.(ts|tsx)$/,
exclude: /node_modules/,
resolve: {
extensions: ['.ts', '.tsx', '.js', '.json'],
},
use: 'ts-loader',
},
// ...
],
},
// ...
}
7. Add scripts to package.json
Add the following start
and build
scripts to your package.json
:
package.json (Linux, OS X)
{
...
"scripts": {
"start": "webpack serve --port 3000",
"build": "NODE_ENV=production webpack"
}
...
}
package.json (Windows PowerShell)
The build
command has to be different:
{
...
"scripts": {
"start": "webpack serve --port 3000",
"build": "set NODE_ENV=production && webpack"
}
...
}
-
start
: Launches a development server on port 3000. The dev server automatically watches your files and rebuilds the app as needed. -
build
: Builds your app for production. TheNODE_ENV=production
sets theNODE_ENV
variable, which is checked in the first line ofwebpack.config.mjs
.
8. Create index.html template
HtmlWebpackPlugin
can generate an HTML file even without a template. However, you'll likely need one, so let's create it in the project root directory. This is the file we referenced in the webpack.config.mjs
plugins section.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="initial-scale=1, width=device-width"/>
<title>Hello React</title>
</head>
<body>
<div id="app-root">App is loading...</div>
</body>
</html>
Notes:
- Remember to update
lang="en"
to the actual language of your application, if it differs - The
<meta name="viewport" .../>
tag is essential for responsive design, enabling your layout to adapt to various device sizes - If you're targeting IE, be sure to add also
<meta http-equiv="x-ua-compatible" content="ie=edge"/>
in the<head>
section
9. Install React
From the project root directory, run the following command:
npm i react react-dom
And then:
npm i --save-dev @types/react @types/react-dom
10. Create src/index.tsx
This is the entry point of your application, which we referenced in webpack.config.mjs
. You can also update the main
field in your package.json
to point to the same file, though it's not strictly necessary.
src/index.tsx
import React from 'react'
import { createRoot } from 'react-dom/client'
const container = document.getElementById('app-root')!
const root = createRoot(container)
root.render(<h1>Hello React!</h1>)
Note: The createRoot()
API is new to React 18. If you are using an older version of React, you can refer to this blog post for guidance and use the following code:
src/index.tsx in React 17
import React from 'react'
import ReactDOM from 'react-dom'
ReactDOM.render(
<h1>Hello React!</h1>,
document.getElementById('app-root'),
)
11. Create src/index.css
and import it to src/index.tsx
To ensure our CSS plugin works, let's apply some simple styles.
src/index.css
body {
color: blue;
}
src/index.tsx
import './index.css'
// The rest app remains the same
// ...
12. Run dev server
It's been a long path, but we're almost there! Let's run the development server:
npm start
Now open http://localhost:3000/ in your browser — you should see the colored greeting:
Hello React!
Now try modifying src/index.tsx
, for example, change the message. The app should reload and show the updated text. You can also try changing the styles — they should be picked up without needing to restart the server.
13. Build your app for production
From the project root directory, run the following command:
npm run build
This will generate a dist
folder containing the bundled files. To serve the files as they would appear in production, you'll need a small utility called serve
and a corresponding script. Add the dependency:
npm i --save-dev serve
Next, update your scripts in package.json
:
package.json
{
...
"scripts": {
...
"preview": "serve dist -p 3000"
},
...
}
...and run npm run preview
. Open http://localhost:3000/ — you should see the greeting!
14. Targeting older environments
This part is a bit more advanced, so feel free to come back to it once you're comfortable with the basic setup.
14.1. Target ES version
The target ES version is set in two places:
-
tsconfig.json
: undercompilerOptions.target
-
webpack.config.mjs
: in theesbuild-loader
configuration
It depends on your app's audience. So, who is your target user?
-
You and your team — If you're using modern tools, it's safe to leave it as
esnext
. You probably don’t need anything outdated 🙂 -
Average Internet user — I’d recommend targeting
es<currentYear-3>
. For example, at the year of this writing, which is 2021, you would targetes2018
. Why notesnext
? Sometimes even seemingly modern browsers miss support for certain features. For instance, Xiaomi MIUI Browser 12.10.5-go, released in May 2021, doesn’t support the nullish coalescing operator. Here's a pen to test it in that browser. What’s your result? -
IE users — If you're supporting Internet Explorer, the target must be
es5
. Be aware that some ES6+ features can get bloated when transpiled to ES5.
14.2. Select target libs
The libraries are set in tsconfig.json
under compilerOptions.lib
, and this option also depends on your guess about your target user.
Typical libs you may use:
-
dom
— Includes all APIs provided by the browser. -
es...
, for example,es2018
— Includes JavaScript built-ins that come with the corresponding ES specification.
Important: Unlike Babel, these options don’t automatically add any polyfills. So, if you’re targeting older environments, you will need to manually add polyfills, as described in the next section.
14.3. Add polyfills
Polyfills are required depending on the APIs your app needs.
- React requires polyfills for:
- If your client-side code uses relatively new APIs, such as flatMap or fetch, while targeting older browsers, consider polyfilling them as well.
Here are some popular polyfills you may use:
-
core-js — for missing
Set
,Map
,Array.flatMap
, etc. -
raf — for missing
requestAnimationFrame
. -
whatwg-fetch — for missing
fetch
. Note: this doesn't include aPromise
polyfill. It’s included incore-js
above.
Given that we decided to use all of them, the setup is as follows:
npm i core-js raf whatwg-fetch
index.tsx
import 'core-js/features/array/flat-map'
import 'core-js/features/map'
import 'core-js/features/promise'
import 'core-js/features/set'
import 'raf/polyfill'
import 'whatwg-fetch'
// The rest app remains the same
// ...
14.4. Tweaking webpack
runtime
It may be surprising, but even if you transpile to ES5, webpack
may still emit code containing ES6+ constructs. So, if you are targeting older browsers like Internet Explorer, you might need to turn off these features. For more details, check this documentation page.
Is it fair to add so many polyfills?
No, it's not always justified, as most users have modern browsers. Adding unnecessary polyfills wastes both runtime and bandwidth. The best approach would be to create two separate bundles: one for modern environments and another for older ones, and then load only the appropriate one based on the user's environment. This approach is beyond the scope of this tutorial.
You're done!
I know that wasn't easy 😅 But I'm sure these concepts are no longer a puzzle for you. Thanks for staying with me throughout this journey!
What's next?
Once you've completed this setup and your app's basic logic, you might need some kind of SSR. The next post in this series explores a convenient and useful way to prerender your app without requiring server-side workers. Have fun!
Top comments (17)
Great resource!
Just wanted to make a small addition regarding the build script:
On windows, this doesn't work. Replacing it with:
will make it work on windows.
Cheers :)
Thanks, added a different fragment for Windows
@alekseiberezkin awesome article, thankyou!
Excellent resource! Thanks for keeping it up to date!
Great job. congrats
@alekseiberezkin is there any chance of changing webpack for esbuild? thanks for the article
Thx for the suggestion. You are right, esbuild is not more an “emerging” tool but a comprehensive alternative. Unfortunately I can't give any estimate when I'm able to look at it closely because right now I'm not working in frontend fulltime. However if I decide writing about something here, that'd be among the first topics to consider.
Hi, I've added the chapter 6.1. about using
esbuild-loader
in the existing webpack setup. I believe it's the best compromise between esbuild speed and webpack maturity.Please add
meta name="viewport" content="width=device-width, initial-scale=1"
to index.html
I spent a lot of time trying to understand why 'min-width' doesn’t work for me.
Thanks for the suggestion, added to section 8
great.
12.1 - compilerOptions.ta*R*get 😉
Thx, fixed
@alekseiberezkin Thank you for the article. Will definitely try this. Meanwhile, I have a similar article, that you might like - React from scratch
Thanks for your link. Will keep it in mind if I need Babel.
Thanks @alekseiberezkin for the tutorial, it was really helpful but I think you missed to add
@types/react
and@types/react-dom
Thank you so much, updated the post