React - Webpack5/Typescript/Testing setup from scratch to Production - (Updated 2021)
In this, I will layout the steps needed to create your react app from scratch using webpack. I won't explain everything in detail, however, I'll try to give you an understanding of concepts separately and how you may use them. These steps assume that you have Node setup and at least some bare-bones knowledge of React.
We'll Focus on
React.Js
WebPack
WebPack & React
Dev Server
Webpack Loaders
Hot Reloading
Building For Production
Typescript Integration
Testing Environment
Deployment
ReactJs
Let's start with React and see how it works as a library. Follow the steps below.
- Create a folder with your
project_name
&& cd into your project directory - Run
yarn init -y
to add a package.json file Run
yarn add react react-dom
Note:
package-lock.json
will be created these enable new developers to get the same version as everyone else.Create a folder named dist at the root and add an index.html file then add a script tag pointing to this React file in the node_modules just before the closing
</body>
tag. Should look like thisNote:
<script src="../node_modules/react/umd/react.development.js"></script>
Just below it add another script tag to create a element. Takes three args: an HTML tag name, props and children.
<script> console.log(React.createElement('h1', null, 'Hello from react!')) </script>
Reload the browser and check the logs, a Js object will be printed in the
console.log
but can't be rendered to screen yet. That's what ReactDOM is going to do for us.- ReactDOM has a render method which takes two arguments: A React Element (the React.createElement()) and an HTML element container to wrap our React Element.
Just like React, we'll also add a script tag for ReactDOM. It's somehow similar
<script src="../node_modules/react-dom/umd/react-dom.development.js"></script>
Add a
<div />
tag to thebody
and give it an id="root". This div's id value will be ReactDOM render's second argument.So let's mashup React and ReactDOM to render something on the screen.
<script> ReactDOM.render(React.createElement('h1', null, 'Hello from react!'), document.getElementById("root")) </script>
Reload the screen and voila!!
Webpack
Webpack bundles all our js into static files which can easily be run by the browser while also minifing the code for optimization.
- Create a new folder named src at the root of the project. We intend to bundle all our js in this src folder and inject it into the index.html
- Install webpack as a dev-dependency
yarn add webpack -D
Let's put webpack to a test, add an
index.js
file to the src folder. Add the following logconsole.log("Hello Webpack")
. In your terminal enter the following command./node_modules/.bin/webpack --mode=development ./src/index.js
. This command is divided into three- ./node_modules/.bin/webpack - Running webpack
- --mode=development - specify development mode
./src/index.js - specify what we are bundling
After running this command we will notice that
dist/main.js
is created at the root directory.dist/main.js
contains our bundled code, to run it simply require it with the<script />
tag inside ourdist/index.html
. Refresh and check your console "hello Webpack". An example to bundle multiple files, create another file insrc/
named maybeindex2.js
add any console.log statement, then require it insideindex.js
(addrequire(index2.js)
at the top of the file) then rerun the command./node_modules/.bin/webpack --mode=development ./src/index.js
, refresh and check the console.
Webpack & React
Now that we have seen React & Webpack work separately, let's mash them up.
- Empty
src/index.js
and copy the React code from the script tag indist/index.html
tosrc/index.js
. Also remove all script tagsdist/index.html
except one with pathdist/main.js
. You may also delete index2.js if you created it. - At the top of
src/index.js
require React and ReactDOM i.e.const React = require('react'); const ReactDOM = require('react-dom');
Configurations We won't have to run that webpack command all the time so let's automate it.
- First we need an entry and output file. Entry file is that which requires all the other files (also files that require others and so on) and the output file is where our bundled code goes. We already have these files; entry -
src/index.js
, output -dist/main.js
- Create our configuration file a
webpack.config.js
at the root of the project and add.module.exports = { mode: 'development', entry: __dirname + '/src/index.js', output: { path: __dirname + '/dist', filename: 'main.js', publicPath: '/', } }
we have extracted variables from our inline command (
node_modules/.bin/webpack --mode=development src/index.js
) and added them to our configuration. - To run our command all we need is webpack command
node_modules/.bin/webpack
Dev Server
It may get tiring to reload the page manually whenever a change is made. DevServer makes it possible for our application to automatically reload when changes are made to our code.
- First we'll install the dev server
yarn add webpack-dev-server -D
. Add this to the root of our
webpack.config.js
configuration file before the closing bracedevServer: { contentBase: './dist', historyApiFallback: true, inline: true }
contentBase - specifies the folder to serve
historyApiFallback - makes it possible for an SPA to have navigation with multiple pages
inline - Automatically refresh the page on file changesAdd
"start": "webpack serve"
to package.jsonscript
.- Run
yarn start
- here on, as long as your devServer is running, any changes to the code will trigger a browser refresh.
Webpack Loaders
Babel loader - We will need to write ES6 as opposed to ES5 (what we've been writing). Unfortunately, not all browsers support ES6, to enable this support we'll need a transpiler to convert our written ES6 to ES5 which is understandable by the other browsers. This is where babel comes in. We are going to add babel to our webpack build process so that we can transpile our code before bundling.
Firstly, run
yarn add @babel/core babel-loader @babel/preset-env @babel/preset-react babel-plugin-transform-class-properties -D
to install the babel and other plugins.- babel-loader- is the most important among the bunch, it gets the code, transpiles it using the provided presets(preset-react and preset-env in this case), then pass it for bundling.
- babel-plugin-transform-class-properties- enables us to use arrow functions outside render and also limit the reference scope of
this
to the App not global. In other words, preventing us from usingbind(this)
everywhere
- babel-loader- is the most important among the bunch, it gets the code, transpiles it using the provided presets(preset-react and preset-env in this case), then pass it for bundling.
Add a
module
object with arules
array to the root of our webpack.config.js file. In the rules, array is where we add an object to specify the loader configurations.module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-react', '@babel/preset-env'], plugins: ['transform-class-properties'] } } } ] },
test: Specifies the types of files to be transpiled
exclude: What should be skipped
loader: What our loader is called
options: What presets and plugins will be used by the loaderRun
yarn start
, if there are no errors - let's proceed to add React code. In case of any errors, the webpack configuration structure has been changing over time so check out their documentation.- In our
src/index.js
change theReact.createElement(...)
to<h1>...</h1>
. also convert the require statements to import statements. - From now on you are free to use React.
Hot Reloading
Imaging reloading our application while preserving the current state and not reloading its entirety. Hot reloading enables you to update specific parts of the UI without reloading everything on the web app.
We'll be using react-hot-loader
Steps to add hot reloading
- Install react-hot-loader
yarn add react-hot-loader
Add
react-hot-loader/babel
to babel plugins inwebpack.config.js
to make sure that our hot reloaded files are compiled by babelmodule.exports = { ... module: { rules: [ { ... options: { ... plugins: ['transform-class-properties', 'react-hot-loader/babel'] } } ] }
Make the root component hot exported - this will send a new version of our app to ReactDOM.render when our files change
import React from 'react' import { hot } from 'react-hot-loader/root'; const App = () => { return ( <div> <h1>Hello Webpack</h1> </div> ) } export default hot(App)
Finally to turn on hot reloading, add the
--hot
flag to your start script inpackage.json
// package.json "scripts": { ... "start": "node_modules/.bin/webpack-dev-server --open --hot" },
Or add
hot: true
to devServer in ourwebpack.config.js
// webpack.config.js devServer: { ... hot: true },
Hot reloading should be working now. Try editing the text, you will notice that it will update without reloading the page.
But that's not all, because it does not support React Hooks. Let's add that support. We'll use @hot-loader/react-dom
to replace react-dom
because it has support for hot loading. Here is how.
- Install the package
yarn add @hot-loader/react-dom -D
Add it to webpack alias in
webpack.config.js
module.exports = { ... resolve: { alias: { 'react-dom': '@hot-loader/react-dom', }, } }
After this Hot reloading should be working fine with React Hooks.
For more check the documentation
Building For Production
We've so far been working with development, now let's focus on deployment to production. When deploying we need to focus on the bare minimum of files required by the remote server to run our app.
- An index.html file (Minified)
- A CSS file (Minified)
- A Javascript file (Minified)
- All image assets
- An asset manifest
Firstly, we'll use webpack to minify the index.html file and automatically add the appropriate style links and script tags, then add it to a build folder. To do this make a copy of our webpack.config.js
file, name the new copy webpack.config.prod.js
, as you might have already guessed this will contain our webpack configurations for production. We are going to make a few tweaks to this file since it contains things that may not be needed for production such as the dev-server and hot-reloading.
- Remove all things concerning the devServer and hot-reloading i.e. the devServer key and its object value, also the
react-hot-loader/babel
value in the babel plugins array. - Since we are using the build folder, this time we'll change the pathname in output to
__dirname + '/build'
and also change the file tobundle.js
(This one is optional) - Set the environment to production to avoid react warnings. require
webpack
and add this to the plugins array at the root of our webpack.config.prod.js.plugins: [ new webpack.DefinePlugin({ 'process.env': { NODE_ENV: JSON.stringify('production') } }), ]
- Run
yarn add html-webpack-plugin -D
to installhtml-webpack-plugin
which will make the html minification for us. - Require
html-webpack-plugin
in ourwebpack.config.prod.js
and add this below to our plugins array at the root of the object.new HtmlWebpackPlugin({ inject: true, template: __dirname + "/dist/index.html", minify: { removeComments: true, collapseWhitespace: true, removeRedundantAttributes: true, useShortDoctype: true, removeEmptyAttributes: true, removeStyleLinkTypeAttributes: true, keepClosingSlash: true, minifyJS: true, minifyCSS: true, minifyURLs: true, }, }),
inject
- simply inserts a script tag inside our minified code. as we'll see
template
- Points to what we are minifying
minify
- minification specifications
Our webpack.config.prod.js should look something like this
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'production',
entry: __dirname + '/src/index.js',
output: {
path: __dirname + '/build',
filename: 'bundle.js',
publicPath: '/'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-react', '@babel/preset-env'],
plugins: ['transform-class-properties']
}
}
}
]
},
resolve: {
},
plugins: [
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify('production')
}
}),
new HtmlWebpackPlugin({
inject: true,
template: __dirname + "/dist/index.html",
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
})
]
}
Removed the alias object in resolve
Let's see how it works. first, add this line
"build": "webpack --config webpack.config.prod.js"
toscripts
in thepackage.json
. The--config
fag specifies the configuration file to use. Runyarn build
to see what happens.You'll notice a directory
build/index.html
created, When you openindex.html
. you'll see the minified code however the problem is that an extra script tag pointing to./main.js
is added. This is because of HtmlWebpackPlugin's inject key. To remove this problem, head back to yourdist/index.html
and delete the script tag point tomain.js
, since as we already noticed one is injected for us automatically. Also, copy our HtmlWebpackPlugin towebpack.config.js
but without theminify
key then rerunyarn build
.
Adding css
If you haven't noticed already your app can't render CSS. Let's enable this.
- Run
yarn add css-loader style-loader -D
check the documentation for more here - Add this object to our
rules
array in both webpack config files underbabel-loader
{ test: /\.css$/, use: ['style-loader', 'css-loader'] },
Here the loader grabs our CSS files and injects them into our javascript
And that's all you need for CSS, go on to test!!
Uglifying/Minifying to improve performance
Note: This section is irrelevant since webpack 4+ minifies your code by default in production. But if you want to look, here is a great webpack optimization tool terser(Installed automatically for webpack 4+) as opposed to what we used uglifyjs. Also uglifyjs below is deprecated feel free to skip it.
Now to our javascript in bundle.js, you'll notice that it spans a number of lines which is not great for performance. We are going to limit it to only one line to make it faster. To achieve this, we are going to run yarn add uglifyjs-webpack-plugin -D
, require it and then add this to the root of our object
optimization: {
minimizer: [
new UglifyJsPlugin({
sourceMap: true,
uglifyOptions: {
warnings: false,
output: {
comments: false,
},
},
}),
],
},
- Run
yarn build
, you'll notice a single line of js in bundle.js
Handling Files (we ain't done ๐ )
Onto adding a file-loader, we also want to make sure that our assets are copied from dist
to build
, to make sure they are accessible in production.
- Let's run
yarn add file-loader -D
to install the file-loader - Add this to both our webpack files
File-loader only deals with files required by Javascript, but how about those required by our{ test: /\.(png|jpe?g|gif)$/i, loader: 'file-loader', }
index.html
like the favicon. To achieve this we'll not use webpack but javascript. - Create a folder
scripts
at the root of the project and a filecopy_assets.js
- Run
yarn add fs-extra
to install fs-extra, require it in ourcopy_assets.js
- Add a script to copy all files in
dist
exceptindex.html
tobuild
.copy_assets.js
should look like this
const fs = require('fs-extra');
fs.copySync('dist', 'build', {
dereference: true,
filter: file => file !== 'dist/index.html'
})
- Change build in
package.json
tonode scripts/copy_assets.js && webpack --config webpack.config.prod.js
and runyarn build
Caching
Add an Asset Manifest We want to list all static assets that we are generating, this will be useful when we start caching them to save load times.
- Run
yarn add webpack-manifest-plugin -D
, requireWebpackManifestPlugin
a named import from webpack-manifest-plugin and then use it in our plugins for the webpack.config.prod.js file.
...
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
module.exports = {
...
plugins: [
...
new ManifestPlugin({
fileName: 'asset-manifest.json',
})
}
- Run
yarn build
an asset-manifest.json will be created in the build folder. openingbuild/index.html
will produce the same outcome as when you runyarn start
Typescript Integration
For details on setup check the webpack docs
We begin by installing Typescript, ts-loader(Typescript loader) and some types as dev dependencies
- Run
yarn add -D typescript ts-loader @types/react @types/react-dom @types/jest @types/html-webpack-plugin
- Create your tsconfig.json file at the root and add the configurations below
{ "compilerOptions": { "outDir": "./dist/", "sourceMap": true, "noImplicitAny": true, "module": "es6", "target": "es5", "jsx": "react", "allowJs": true, "strict": true, "esModuleInterop": true, "moduleResolution": "node" } }
- Update the
webpack.config.js
andwebpack.config.prod.js
.- Change entry point to
__dirname + '/src/index.tsx'
- Swap the babel configurations with the
ts-loader
- Switch /.js$/ with /.tsx$/
- Add an array of extensions inside the resolve object
module.exports = { ... entry: __dirname + '/src/index.tsx', devtool: 'inline-source-map', ... module: { rules: [ { test: /\.tsx$/, exclude: /node_modules/, use: 'ts-loader', }, ... ] }, resolve: { extensions: ['.tsx', '.ts', '.js'], ... }, ... }
- Change entry point to
- Currently, we can't use assets like images, to enable that we create a
custom.d.ts
file at the root of our project and add the following declarations.
declare module '*.svg';
declare module '*.png';
declare module '*.jpg';
declare module '*.jpeg';
- Change all files inside
src
totsx
andts
respectively - Run
yarn start
That's all we need to integrate Typescript!!
Testing Environment
We are going to integrate the react-testing-library(why it's better than enzyme). We'll start by installing jest, ts-jest, @testing-library/react and their corresponding types and dependencies.
- Run
yarn add -D ts-jest jest jsdom @types/jest @testing-library/react @testing-library/jest-dom
- Run
yarn ts-jest config:init
to generate ourjest.config.json
with some default configurations. - Inside
jest.config.json
change thetestEnvironment
value to 'jsom' - Jest will naturally render our static data as ts files which will cause errors, so we'll have to use mocks, to tell jest to use the mocks whenever attempting to access static files. Create a mocks folders at the root of your project and inside it add a fileMock.ts file.
- Inside the
jest.config.json add a
moduleNameMapperfield to specify all static file types
.
// jest.config.json
testEnvironment: 'jsdom',
moduleNameMapper: {
"\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.ts",
"\\.(css|less)$": "<rootDir>/__mocks__/fileMock.ts"
}
// fileMock.ts
module.exports = '';
That's all we need to integrate the testing environment, now to run the tests we'll add a script inside the package.json.
{
...
"scripts": {
"test": "yarn jest --coverage",
...
},
...
}
To test this, let's write a simple failing test and run it. Inside our src create a file named App.spec.tsx
.
import React from 'react'
import { render } from '@testing-library/react'
import App from "./App"
describe('App.js', () => {
test('should be true', () => {
expect(true).toEqual(true)
})
test('should fail until we add 'hello world' in our document' , () => {
const { getByText, container } = render(<App />)
expect(getByText("hello world")).toBeInTheDocument()
})
})
Run yarn test
, tests should fail
To make that pass add a hello world
tag inside your App.tsx
Deployment
We going to deploy our app to vercel, but first, let's initialize our project as a github repo.
- Run
git init
(hopefully you have git install and have a github account) - Create a
.gitignore
and add irrelevant files like the node_modules directory - Run
git add .
thengit commit -m "initial commit"
- Create a new repo and push the codebase.
Vercel
- Create a vercel account (if you don't have one) and connect your github account.
- Select the repo you wish to deploy.
- In the
Build and Output Setting
overrideOUTPUT DIRECTORY
and addbuild
if it isn't already. - Deploy!!
THAT'S ALL
Refer to the complete code here: github.com/ezrogha/react-webpack-typescript
Congrats !!!
If you get stuck at any stage, have suggestions or questions, feel free to drop a comment.
References