In this blog, we will see how to set up a React app from scratch using Typescript, Webpack, Babel, Eslint, and prettier. Although the setup given by create-react-app is great and optimized well, it’s good to know what is the job of each tool and how to configure them by yourself that way you can have full control and customize them according to your project need.
React app setup
Index
- Creating a git repo
- Initializing basic web project
- Adding npm
- Setting up babel
- Adding webpack
- Setting up eslint and prettier
Step 1: Creating an git repo
We will initialize a git repository. So inside the folder which will contain all your project run:
git init
Create a file called .gitignore and these two folder names, so they won’t be tracked.
build
node_modules
Step 2: Initializing a basic web project
Create folders “src” and “build”. Inside the src folder create a file index.html. In the index.html file add an HTML starter template with this line inside the body. <div id=”root”></div>. And this div will contain all the react components that will be rendered. And this index.html will be out of our single-page web app.
src/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>React ts</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
Step 3 : Adding npm
To manage all the packages and dependencies we need to have a package.json file to run the command.
npm init -y
Note: we can change the meta info in package.json file like name, version description etc late
Now we will install the hero of the app. That’s right… React! So run command
npm i react react-dom
Note: at the time of writing this blog latest version was React 17.
No, since we will be going to use Typescript language to code our react components.
npm i -D typescript @types/react @types/react-dom
“typescript” is the core package that lets you use typescript.
“@types/react @types/react-dom” are the types for React and React-Dom functions.
Create a file “tsconfig.json” in the root directory to give the compiler options for type checking.
tsconfig.json
{
"compilerOptions": {
"target": "ES5" /* Specify ECMAScript target version */,
"module": "ESNext" /* Specify module code generation */,
"moduleResolution": "node" /* Specify module resolution strategy */,
"lib": [
"DOM",
"ESNext"
] /* Specify library files to be included in the compilation. */,
"jsx": "react-jsx" /* Specify JSX code generation */,
"noEmit": true /* Do not emit outputs. */,
"isolatedModules": true /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */,
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
"strict": true /* Enable all strict type-checking options. */,
"skipLibCheck": true /* Skip type checking of declaration files. */,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
"allowJs": true /* Allow javascript files to be compiled */,
"checkJs": true /* Report errors in .js files */,
},
"include": ["src/**/*"]
}
Important note
the compiler options are focused on type checking and not transpiling. We will use babel for that.
Create 2 files in “src” folder “index.tsx” and “App.tsx”
src/App.tsx
import React from 'react'
export const App = () => {
return (
<div>
Hello, this is my React app.
</div>
)
}
src/index.tsx
import ReactDOM from 'react-dom';
import { App } from './App';
ReactDOM.render(<App/>, document.getElementById('root'));
Step 4: Adding babel
This code cannot be understood by our browser as it. We need babel to covert ts and react to js code.
Babel is a Js compiler that is mainly used to convert ECMAScript 2015+ code into a backward compatible version of JavaScript in current and older browsers or environments.
If you have no idea about what is Babel, you can skim through its documentation quickly.
Run the following command
npm i -D @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript @babel/runtime @babel/transform-runtime-plugin
@babel/core => the core package of babel.
@babel/preset-env => allows you to use the latest JavaScript.
@babel/preset-typescript => for using typescript.
@babel/transform-runtime-plugin => helps you use some modern features of js like async/await.
Create a file in root “.babelrc” for configuring babel
{
"presets": [
"@babel/preset-env",
[
"@babel/preset-react",
{
"runtime": "automatic"
}
],
"@babel/preset-typescript"
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"regenerator": true
}
]
]
}
Step 5: Adding webpack
Now, we will install add webpack to our project.
And why webpack or what is webpack ?
If you have written react code you would know that it requires writing a lot of javascript files and importing these files to other files. We do this using some sort of module system, the two most popular module systems are CommonJS’s require()
and ESM’s import
. Unfortunately, there is no browser support for CommonJs, and support for ESM is not universal yet. So we need a tool that can take all the files that we have written and combine/bundle them into a single js file while keeping track of their dependencies. The tool that does this are called bundlers and webpack is one of the many javascript bundlers!
Learn more about webpack.
Let dive straight into setting up webpack
Let’s create together the config files and install packages step by step to understand them more clearly. Let’s start with the core packages.
npm i -D webpack webpack-cli webpack-dev-server html-webpack-plugin babel-loader
Create a folder in root named “webpack” and create the following files
webpack/
|- webpack.common.js
|- webpack.config.js
|- webpack.development.js
|- webpack.production.js
The common file will have the common config for both the dev and prod environment and each dev and prod file will have some environment-specific configurations. in the webpack.config.js file, we will merge the common and environment-specific configurations.
webpack/webpack.common.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: path.resolve(__dirname, '..', './src/index.tsx'),
resolve: {
extensions: ['.js', '.ts', '.tsx'],
},
module: {
rules: [
{
// should use babel-loader for all ts js tsx and jsx files
test: /\.(ts|js)x?$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
},
],
},
],
,
output: {
path: path.resolve(__dirname, '..', './build'),
filename: '[name].[chunkhash].js',
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "..", "./src/index.html")
})
],
}
entry specifies from which file to webpack first look and start bundling
resolve here helps to import without extensions and looks for file in order i.e tsx -> ts -> js
module to define rules of how diff module should be loaded.
output to specify where the bundled code should be outputted. The path contains the path of the folder, in this case, build folder we created. And filename tells the name of the file of the bundled code, so it will take the name and the chuck reference of the file and combine them to create a filename for bundled output.
HtmlWebpackPlugin template tells the webpack to use index.html file as a template to add bundled js file to and add index.html to the build folder.
To load a file that is not javascript or typescript like CSS and fonts and images requires different loaders. So run the command to install
npm i -D style-loader css-loader
And add new rules in webpack.common.js.
module.export = {
...
...
...
module: {
rules: [
.
.
.,
{
{
// should use style-loader and css-loader for all css files
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
{
// v5 supports image loaders out of box
test: /\.(?:ico|gif|png|jpg|jpeg)$/i,
type: 'asset/resource',
},
{
test: /\.(woff(2)?|eot|ttf|otf|svg|)$/,
type: 'asset/inline',
},
}
]
},
...
...
...
}
for image files and font files there is out-of-the-box support t from v5. but if you are on lower versions you look up specific loaders.
Also, a help full plugin that can copy assets into your build folder
npm i -D copy-webapck-plugin
const CopyWebpack = require('copy-webpack-plugin')
module.export = {
.
.
.
plugins: [
...
new CopyWebpack({
patterns: [{ from: 'sourcefolder', to: 'destinationfolder' }],
}),
],
}
Now we have most of our webpack config in place. Let’s add the environment config for development and production mode.
webpack/webpack.development.js
const webpack = require('webpack')
module.exports = {
mode: "development",
devtool: "cheap-module-source-map",
plugins: [
new webpack.DefinePlugin({
'process.env.name': JSON.stringify('dev')
})
],
}
webpack/webpack.production.js
const webpack = require('webpack');
module.exports = {
mode: "production",
devtool: "source-map",
plugins: [
new webpack.DefinePlugin({
'process.env.name': JSON.stringify('prod')
})
]
}
So as you can see not much differs from a basic webpack setup.
mode sets the mode of app this will also set the environment variables in the node process.
devtool is used to set that how the js files will be minified. So for the dev environment, we use a less minified version so its easy to debug and source-map for prod which gives very optimized and lightweight build files.
In the webpack.DefinePlugin we modify the node process object which is very useful to do environment-specific variables and functionality in code. In this case, we are not doing much but you can use this to set API URLs. You can access this in any js/ts file by just using the process.env.name directly.
Now we will configure the webpack.config.js file which will merge the common config and one of the environment-specific configs depending on the environment.
npm i -D webpack-merge
webpack.config.js
const { merge } = require("webpack-merge");
const commonConfig = require("./webpack.common.js");
module.exports = (envVars) => {
const { env } = envVars;
const envConfig = require(`./webpack.${env}.js`);
const config = merge(commonConfig, envConfig);
return config;
};
So this file takes the variable that we will pass from the scripts we will add in the package.json to run and build the app using webpack. Let’s see how.
package.json
"scripts": {
... ,
"start": "webpack serve --config webpack/webpack.config.js --env env=dev --open",
"build": "webpack --config webpack/webpack.config.js --env env=prod",
...
},
Add these two new scripts “start” and “build”. The “start” script serves the app locally to port localhost:8080 and we pass env=dev. The “build” script will be used for creating build folder of the app and we pass env=prod.
Note: we install webpack-cli earlier which gives us the ability to use the command webpack.
You can test our progress here by changing the App.tsx file to
import React from 'react'
export const App = () => {
return (
<div>
Hello, this is {process.env.name} app
</div>
)
}
run “npm start” and go to localhost:8080
to check the prod build run the command “npm run build”. You will see the files are added to the build folder and now if you wish you can serve this by using running “npx http-server ./build” whish will spin up a local server and server files.
Step 6: Adding eslint and prettier
Now we will set up eslint. Eslint is not required to run an app but it helps in writing quality code that is less prone to bugs and more readable. I would say must to have if you have a team of developers.
There can be an infinite number of ways you can set up eslint. It completely depends on you, which rules you wanna enforce to check for bugs. There are some already great blogs that you can search to setup eslint in a very strict manner. However, in this blog, I will add a basic one and you can take it from here.
Note: Install the eslint extension for whichever code editor your using.
Let’s install all the dependencies.
npm i -D eslint eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react eslint-plugin-react-hooks
create file .eslintrc.js
.eslintrc.js
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
},
settings: {
react: {
version: 'detect',
},
},
extends: [
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:@typescript-eslint/recommended',
'plugin:import/errors',
'plugin:import/warnings',
'plugin:import/typescript',
'plugin:jsx-a11y/recommended',
],
rules: {
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': ['error'],
'@typescript-eslint/no-var-requires': 'off',
'react/prop-types': 'off',
'react/jsx-uses-react': 'off',
'react/react-in-jsx-scope': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
indent: ['error', 2],
'@typescript-eslint/indent': ['error', 2],
'max-len': ['error', { code: 120 }],
},
}
Now to help you format your code I recommend adding prettier to your project. Which is an opinionated code formatter.
Note: Add prettier extension in your code editor.
npm i -D prettier eslint-plugin-prettier eslint-config-prettier
create file .prettierrc.js
.prettierrc.js
module.exports = {
semi: false,
trailingComma: 'es5',
singleQuote: true,
jsxSingleQuote: false,
printWidth: 80,
tabWidth: 2,
endOfLine: 'auto'
}
Your code will be formatted automatically according to this config and you can prettier rules to eslint as well so there is no conflict. In the .eslintrc.js file in extends add :
.eslintrs.js
extends: [
... ,
'prettier',
'plugin:prettier/recommended',
]
Now if you have any syntax or logical error according to your rules it will who error in your Code editor.
To help us make our life easy so we will add two scripts to check and possibly fix any fixable eslint error.
package.json
scripts: {
... ,
"lint": "eslint --fix \"./src/**/*.{js,jsx,ts,tsx,json}\"",
"format": "prettier --write \"./src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\"",
}
Conclusion
Well done! You have successfully created a react app without create-react-app. Setting up an app from scratch is something developers can wear like a badge of honor. There is a lot more stuff you can change on optimizing but the aim of this blog was to give you a well basic enough setup. Hope you learned something new today.