This documentation was done while learning from egghead course 'modern-javascript-tooling-with-react' authored by Andy van Slaars. I take this opportunity to thank egghead.io and Andy van Slaars.
At egghead.io we can learn the difficult parts of the techologies in a very easy to understand short videos from leading experts. I am very happy with experience and the things i am learning at egghead.io. Click here to signup now
Note: I am assuming you have the git and latest nodejs installed. Open the terminal and create a folder where you want keep the project code
mkdir react-from-scratch
cd react-from-scratch
npm init
A list of config questions will be asked. Answer all of them and submit. Now we have the package.json created in our folder. Lets initialize the git now.
git init
this will initialize the git. Now lets commit the package.json file
git status
git add .
git commit -m "Initial commit"
We have commited the file locally but not yet pushed to the remote. Create a repository in your github repository and it will show the details to push the data from a existing local repository. Copy and paste it in your terminal It will look something like this
git remote add origin https://github.com/sudarshanshenoy/React-from-scracth-setup.git
git push -u origin master
Create a folder with name src and create index.js file with the following content
console.log("Hello World");
Now to bundle the javascript file we have to install Webpack
npm install --save-dev webpack webpack-cli
Now you can see node_modules folder and inside that the there is .bin folder Open the .bin and you can see executable file for webpack Let run and see what happens.
node_modules/.bin/webpack
When you run this a bundle javascript file is generated in the dist folder Lets run the bundle file 'main.js' file
node dist/main.js
This will output 'Hello World'
Rather than running 'node_modules/.bin/webpack' every time we can add it to the scripts in package.json
"scripts": {
"build": "webpack --mode production",
"test": "echo \"Error: no test specified\" && exit 1"
}
Now we can run the following command to build the files
npm run build
Now if we run 'git status' command we can find that there is node_modules, dist and some other files as well. We dont want to add node_modules and dist to github. Create a file called gitignore with the following content to ignore these folders
dist/
node_modules/
First let add a new js file person.js with following content
const person = 'Sudarshan Shenoy'
export default person;
and import the file in index.js
import person from './person';
console.log(`Hello ${person}`);
Create webpack.config.js with following content the entry attribute should have the path to main path from where the webpack has bundle Under output we have path attribute which should contain absolute path to the output folder and so we use the path library from node
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
path: path.join(__dirname, 'dist'),
filename: 'app.bundle.js'
}
}
after this run
npm run build
node dist/app.bundle.js
Let change the index.js and person.js file as follows
index.js:
import person from './person';
console.log(person('sudarshan shenoy'));
person.js:
const person = name => `hey ${name}`
export default person;
Let install babel
npm install --save-dev @babel/core @babel/cli @babel/preset-env
Let try executing the babel on our code
./node_modules/.bin/babel ./src/person.js --presets=@babel/preset-env
This will show that our modern javascript code syntax is transformed
Let put this in webpack.config.js to automate the process of transformation during the build To do that we will need babel loader
npm i -D babel-loader
Add new attribute in the webpack.config.js called module as follows
const path = require('path');
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
path: path.join(__dirname, 'dist'),
filename: 'app.bundle.js'
},
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/,
options: {
presets: ['@babel/preset-env']
}
}
]
}
}
As you see module contains rules which is a array. Under rules the test key has a regex expression which say get me all the files which match this regex and give that as the input to the babel. Next exlude says not to include the files inside node_modules as metioned. Options has the option we have to give when we run the babel command
To install react run the following command
npm install react react-dom prop-types
change index.js to
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App'
ReactDOM.render(<App/>, document.getElementById('app'))
create a new file App.js
import React from 'react';
class App extends React.Component {
render() {
return <h1>Hello World</h1>
}
}
export default App;
Now if we try to build this it will throw a error as it encounters JSX syntax. To solve this we will need one more loader
npm i -D @babel/preset-react
add @babel/preset-react to options in the webpack.config.js
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/,
options: {
presets: ['@babel/preset-env', '@babel/preset-react']
}
}
]
}
If you run the 'npm run build' the js file is bundled. If you run the build it gives a error as it could not recognise document.getElementById. To solve this we need HtmlWebpackPlugin as dev dependency
npm i -D html-webpack-plugin
Update the webpack.config.js file as follows
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
path: path.join(__dirname, 'dist'),
filename: 'app.bundle.js'
},
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/,
options: {
presets: ['@babel/preset-env', '@babel/preset-react']
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
})
]
}
Create a index.html file in in the src folder
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Page Title</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div id="app"></div>
</body>
</html>
run build and open the index.html generated in the dist folder. You will see that react is installed and up and running
Add the following line under scripts in package.json
"dev": "webpack --watch --mode development",
and run
npm run dev
When ever there is a change it will rebuild
Install webpack merge
npm i -D webpack-merge
Instead of one webpack.config.js we will have 3 files
- webpack.config.base.js
- webpack.config.dev.js
- webpack.config.prod.js
Add the following to respective files
// webpack.config.base.js:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
path: path.join(__dirname, 'dist'),
filename: 'app.bundle.js'
},
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/,
options: {
presets: ['@babel/preset-env', '@babel/preset-react']
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
})
]
}
// webpack.config.dev.js:
const merge = require('webpack-merge')
const baseConfig = require('./webpack.config.base')
module.exports = merge(baseConfig, {
mode: 'development'
})
// webpack.config.prod.js:
const merge = require('webpack-merge')
const baseConfig = require('./webpack.config.base')
module.exports = merge(baseConfig, {
mode: 'production'
})
In the package.json change the script like this
"scripts": {
"build": "webpack --config webpack.config.prod.js",
"dev": "webpack --watch --config webpack.config.dev.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
npm i -D webpack-dev-server
update the dev script in package.json
"dev": "webpack-dev-server --open --config webpack.config.dev.js",
open flag will make sure evertime its run it will open a new tab.
I can specific the port the webpack dev server to run by updating the webpack.config
// webpack.config.dev.js:
const merge = require('webpack-merge')
const baseConfig = require('./webpack.config.base')
module.exports = merge(baseConfig, {
mode: 'development',
devServer: {
port: 3000
}
})
just add devtool: 'source-map'
// webpack.config.dev.js:
const merge = require('webpack-merge')
const baseConfig = require('./webpack.config.base')
module.exports = merge(baseConfig, {
mode: 'development',
devServer: {
port: 3000
},
devtool: 'source-map'
})
Install plugin
npm i -D @babel/plugin-proposal-class-properties
and plugin under options for babel in webpack.config.base.js
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
plugins: ['@babel/plugin-proposal-class-properties']
}
Change the content of App.js
class App extends React.Component {
state = {
count: 0
}
render() {
return (
<div>
<h1>Hello World</h1>
<h2>Count: {this.state.count}</h2>
<button onClick={() => this.setState(state => ({count: state.count + 1}))}>+</button>
<button onClick={() => this.setState(state => ({count: state.count - 1}))}>-</button>
</div>
)
}
}
export default App;
Create a index.css with following content
button {
background-color: rgb(80, 109, 240);
color: #fff
}
In index.js import css file
import './index.css'
We will need loaders else the webpack load be able to load
npm i -D css-loader style-loader
In the wepack.config.base.js add the following similar to the babel loaders under module
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
exclude: /node_modules/
}
Now run
npm run dev
Hot reload makes sure that the changes in the code is reflect with losing the state.
npm i -S react-hot-loader
Add a plugin in the webpack base
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/,
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
plugins: [
'react-hot-loader/babel',
'@babel/plugin-proposal-class-properties'
]
}
Import hot from react hot reloader in App.js
import React from 'react';
import {hot} from 'react-hot-loader';
class App extends React.Component {
state = {
count: 0
}
render() {
return (
<div>
<h1>Hello World</h1>
<h2>Count: {this.state.count}</h2>
<button onClick={() => this.setState(state => ({count: state.count + 1}))}>+</button>
<button onClick={() => this.setState(state => ({count: state.count - 1}))}>-</button>
</div>
)
}
}
export default hot(module)(App);
Add new script
"dev:hot": "webpack-dev-server --open --hot --config webpack.config.dev.js",
add cdn to your index.html template file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Page Title</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div id="app"></div>
<% if(process.env.NODE_ENV === 'production') { %>
<script crossorigin src="proxy.php?url=https%3A%2F%2Funpkg.com%2Freact%4016%2Fumd%2Freact.production.min.js"></script>
<script crossorigin src="proxy.php?url=https%3A%2F%2Funpkg.com%2Freact-dom%4016%2Fumd%2Freact-dom.production.min.js"></script>
<% } %>
</body>
</html>
In webpack base add externals key as show below
const merge = require('webpack-merge')
const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer')
const baseConfig = require('./webpack.config.base')
module.exports = merge(baseConfig, {
mode: 'production',
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
reportFilename: 'bundle_sizes.html'
})
],
externals: {
react: 'React',
'react-dom': 'ReactDOM'
}
})
Install babel polyfill
npm i -S @babel/polyfill
npm i -D [email protected]
import @babel/polyfill in index.js
import '@babel/polyfill'
update the webpack base config
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
path: path.join(__dirname, 'dist'),
filename: 'app.bundle.js'
},
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/,
options: {
presets: [[
'@babel/preset-env',
{
targets: [
'last 2 versions',
'not dead',
'not < 2%'
],
useBuiltIns: 'entry'
}
], '@babel/preset-react'],
plugins: [
'react-hot-loader/babel',
'@babel/plugin-proposal-class-properties'
]
}
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
exclude: /node_modules/
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
})
]
}
create a new file warning.js
import React from 'react'
export default () => <span classname={'warning'}> Warning </span>
Install plugin to support dynamic loading and add it to plugins in webpack config
npm i -D @babel/plugin-syntax-dynamic-import
// In webpack base config
plugins: [
'react-hot-loader/babel',
'@babel/plugin-proposal-class-properties',
'@babel/plugin-syntax-dynamic-import',
]
Lazy load the react components in App.js
import {hot} from 'react-hot-loader';
import Warning from './Warning';
import './index.css';
const Warning = React.lazy(() => import('./Warning'))
class App extends React.Component {
state = {
count: 0
}
render() {
return (
<div>
<h1>Hello World</h1>
<h2>Count: {this.state.count}</h2>
<button onClick={() => this.setState(state => ({count: state.count + 1}))}>+</button>
<button onClick={() => this.setState(state => ({count: state.count - 1}))}>-</button>
{
this.state.count > 10 ?
<React.Suspense fallback={null}>
<Warning />
</React.Suspense>
: null
}
</div>
)
}
}
export default hot(module)(App);
npm i -D jest
create a file App.spec.js
describe('App', () => {
it('Runs and passes', () => {
expect(true).toBe(true)
})
})
Add script in package.json
"test": "jest"
run the test script.
Next to test react components we will need the following
npm i -D react-testing-library jest-dom
change the App.spec.js
import React from 'react';
import { render } from 'react-testing-library'
import 'jest-dom/extend-expect'
import 'react-testing-library/cleanup-after-each'
import App from './App'
describe('App', () => {
it('Render with error', () => {
render(<App />)
})
})
move the options under babel config to .babelrc file
{
"presets": [[
"@babel/preset-env",
{
"targets": [
"last 2 versions",
"not dead",
"not < 2%"
],
"useBuiltIns": "entry"
}
], "@babel/preset-react"],
"plugins": [
"react-hot-loader/babel",
"@babel/plugin-proposal-class-properties",
"@babel/plugin-syntax-dynamic-import",
]
}
Add more libraries
npm i -Dbabel-plugin-dynamic-import-node
Handling css import
1. Add to package.json
"jest": {
"moduleNameMapper": {
"\\.(css|less|sass|scss)$": "<rootDir>/__mocks__/styleMock.js",
"\\.(gif|ttf|eot|svg)$": "<rootDir>/__mocks__/fileMock.js"
}
}
2. Create a folder __mocks__ and create fileMock.js and styleMock.js
fileMock.js:
module.exports = 'test-file-stub';
styleMock.js
module.exports = {};
Now run the test
npm run test
Remove the following import from App.spec.js and move to testSetup.js in root folder and to avoid repeating in all the files
import 'jest-dom/extend-expect'
import 'react-testing-library/cleanup-after-each'
some more setup is needed to make this work. create jest.config.js file
module.exports = {
setupTestFrameworkScriptFile: '<rootDir>/testSetup.js'
}
npm i -D prettier pretty-quick
add a script in the package.json
"format": "pretty-quick"
add .prettierrc
{
"tabWidth": 2,
"semi": false,
"singleQuote": true
}
add .prettierignore
dist
package-lock.json