React with Typescript and Webpack
Aug 23, 2019 • 17 Minute Read
Introduction
React is widely used today for creating interactive apps. However, most of the apps rely on create-react-app CLI and use ES6. In this guide, we will look at creating React app using Webpack and using TypeScript. The upside of this is greater control over our project and we also get all of the benefits of TypeScript.
Create a New Project
Let's start by creating a new directory for our project.
mkdir my-sample-react-ts-webpack
cd my-sample-react-ts-webpack
We'll use npm to initialize our project as below:
npm init -y
The above will generate a package.json with some default values.
Let's also add some dependencies for webpack, typescript, and some React-specific modules.
npm install --save-dev webpack webpack-cli
npm install --save react react-dom
npm install --save-dev @types/react @types/react-dom
npm install --save-dev typescript ts-loader source-map-loader
Let us also manually add a few different files and folders under our "my-sample-react-ts-webpack":
- We'll add webpack.config.js for adding Webpack related configurations.
- We'll add tsconfig.json for all of our TypeScript configs.
- Add server.js for starting our app.
- Add a new directory "src" inside "app".
- We'll also add new directory "components" inside "app".
- Finally, we'll add App.tsx inside "components", and index.tsx and HelloWorld.tsx inside components.
Thus, our folder structure will look something like below:
├── README.md
├── package.json
├── package-lock.json
├── server.js
├── tsconfig.json
├── webpack.config.js
├── .gitignore
└── src
└──app
└──components
├── App.tsx
├── index.tsx
├── HelloWorld.tsx
├── index.html
Start Adding Some Code
We'll start with index.html.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Let's learn to create a React app using Typescript and Webpack</title>
</head>
<body>
<div id="content"></div>
</body>
</html>
The above will create the HTML with an empty div with an ID of "content".
We'll then add some configuration to tsconfig.json.
{
"compilerOptions": {
"jsx": "react",
"module": "commonjs",
"noImplicitAny": true,
"outDir": "./build/",
"preserveConstEnums": true,
"removeComments": true,
"sourceMap": true,
"target": "es5"
},
"include": [
"./src/**/**/*"
]
}
Now, let's add some Webpack configuration to webpack.config.js.
const path = require('path'),
webpack = require('webpack'),
HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
app: ['./src/app/App.tsx', 'webpack-hot-middleware/client'],
vendor: ['react', 'react-dom']
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].bundle.js'
},
devtool: 'source-map',
resolve: {
extensions: ['.js', '.jsx', '.json', '.ts', '.tsx']
},
module: {
rules: [
{
test: /\.(ts|tsx)$/,
loader: 'ts-loader'
},
{ enforce: "pre", test: /\.js$/, loader: "source-map-loader" }
]
},
plugins: [
new HtmlWebpackPlugin({ template: path.resolve(__dirname, 'src', 'app', 'index.html') }),
new webpack.HotModuleReplacementPlugin()
]
}
We can also use variables to dynamically set an attribute based on whether we are running in Production or in Development. Our webpack.config.js would then look like below:
const path = require('path');
const isProduction = typeof NODE_ENV !== 'undefined' && NODE_ENV === 'production';
const mode = isProduction ? 'production' : 'development';
const devtool = isProduction ? false : 'inline-source-map';
module.exports = [
{
entry: './src/client.ts',
target: 'web',
mode,
devtool,
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
options: {
compilerOptions: {
"sourceMap": !isProduction,
}
}
}
]
},
resolve: {
extensions: [ '.tsx', '.ts', '.js' ]
},
output: {
filename: 'client.js',
path: path.join(__dirname, 'dist', 'public')
}
},
{
entry: './src/server.ts',
target: 'node',
mode,
devtool,
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
},
resolve: {
extensions: [ '.tsx', '.ts', '.js' ]
},
output: {
filename: 'server.js',
path: path.resolve(__dirname, 'dist')
},
node: {
__dirname: false,
__filename: false,
}
}
];
Our server.js should look like below:
const path = require('path'),
express = require('express'),
webpack = require('webpack'),
webpackConfig = require('./webpack.config.js'),
app = express(),
port = process.env.PORT || 3000;app.listen(port, () => { console.log(`App is listening on port ${port}`) });app.get('/', (req, res) => {
res.sendFile(path.resolve(__dirname, 'dist', 'index.html'));
});let compiler = webpack(webpackConfig);
app.use(require('webpack-dev-middleware')(compiler, {
noInfo: true, publicPath: webpackConfig.output.publicPath, stats: { colors: true }
}));
app.use(require('webpack-hot-middleware')(compiler));
app.use(express.static(path.resolve(__dirname, 'dist')));
Let's add the code to our React component HelloWorld.tsx:
import * as React from "react";
export interface HelloWorldProps { firstName: string; lastName: string; }
export const HelloWorld = (props: HelloWorldProps) => <h1>Hi there from React! Welcome {props.firstName} and {props.lastName}!</h1>;
The above example is using functional components. We can even use class-based components, like below:
import * as React from "react";
export interface HelloWorldProps { firstName: string; lastName: string; }
// 'HelloWorldProps' describes our props structure.
// For the state, we use the '{}' type.
export class HelloWorld extends React.Component<HelloWorldProps, {}> {
render() {
return <h1>Hi there from React! {this.props.firstName} and {this.props.lastName}!</h1>;
}
}
Now, let's update the code in index.tsx as is shown below:
import * as React from "react";
import * as ReactDOM from "react-dom";
import { HelloWorld } from "./components/HelloWorld";
ReactDOM.render(
<HelloWorld firstName="Chris" lastName="Parker" />,
document.getElementById("content")
);
As we can see, we just imported the HelloWorld component inside our index.tsx. When Webpack sees any file with the extension .ts or .tsx, it will transpile that file using the Typescript loader.
Webpack Configuration
Let's have a look at the different options we added to webpack.config.js.
-
entry - This specifies the entry point for our app. It can be a single file or an array of files that we want included in our build.
-
output - This contains the output config. It looks at this when trying to output bundled code from our project to the disk. The path represents the output directory for code to be outputted to and the filename represents the filename for the same. It is generally called bundled.js.
-
resolve - Webpack looks at this attribute to decide whether to bundle or skip the file. Thus, in our project, Webpack will consider files having extensions '.js', '.jsx', '.json', '.ts', and '.tsx' for bunding.
-
module - We can enable Webpack to load a particular file when requested by the app, using loaders. In our example project, we use ts-loader and source-map-loader. Without ts-loader module, Webpack would be unable to understand the imported information in App.tsx as the HelloWorld component is actually a ‘tsx’ file which our editor is aware of but it is not recognized by Webpack when the import occurs.
import { HelloWorld } from './components/HelloWorld';
- plugins - Webpack has its own limitations. To overcome that, it provides plugins to extend its capabilities e.g. html-webpack-plugin. This plugin creates a template file which is rendered to the browser from the index.html file inside the src directory.
TypeScript Configuration
Let's also have a look at the different options we added to tsconfig.json:
- compilerOptions - Represents the different compiler options.
- jsx:react - Adds support for JSX in .tsx files.
- lib - adds a list of library files to the compilation (e.g. using es2015 allows us to use ES6 syntax).
- module - Generates module code.
- noImplicitAny - Used to raise errors for declarations with an implied "any" type
- outDir - Represents the output directory.
- sourceMap - Generates a .map file which can be very useful for debugging our app.
- target - Represents the target ECMAScript version to transpile our code down to (we can add a version based on our specific browser requirements).
- include - Used to specify the file list to be included.
Build Our Project
We can now build our app manually. We'll also see how to build our app automatically. But for now, let's add a 'build' script to our package.json and then run the Webpack command.
...
scripts: {
...
"build": "./node_modules/.bin/webpack",
...
}
...
We can now go to the command prompt and run the following command:
npm run build
Adding Scripts to package.json
We can add different scripts to build React apps in our package.json, as is shown below:
...
"start": "webpack-dev-server --open",
"devbuild": "webpack --mode development",
"build": "webpack --mode production"
...
Backend
We can use Express as the framework for the backend with EJS as the templating engine. Let's install the same using:
$ npm i express
$ npm i -D @types/express
$ npm i ejs
In our config.ts file, we'll add the following code for the server port:
export const SERVER_PORT = parseInt(process.env.SERVER_PORT || "4200");
Let's add our web module now. To do that, we'll create a new file in ./src/web/web.ts, as is shown below:
import express from "express";
import http from "http";
import path from "path";
// Initialization
const app = express();
// Configuration for templating engine
app.set("view engine", "ejs");
app.set("views", "public");
// Configuration for static files
app.use("/assets", express.static(path.join(__dirname, "frontendproj")));
// Add Controllers
app.get("/*", (req, res) => {
res.render("index");
});
// Add start method
export const start = (port: number): Promise<void> => {
const server = http.createServer(app);
return new Promise<void>((resolve, reject) => {
server.listen(port, resolve);
});
};
So, as we can see, a view directory 'public' is needed and we also need a folder for static files: “frontendproj”.
With the templating engine, we'll now add content to index.ejs inside the public directory.
Let's add the same as below:
<!DOCTYPE html>
<html lang="en">
<head>
<title>React with Typescript and Webpack with EJS Templating Engine</title>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
</head>
<body>
<div id="root"></div>
<script src="/assets/vendors.js"></script>
<script src="/assets/main.bundle.js"></script>
</body>
</html>
We add a couple of script elements for our JS code bundles, which will get built using Webpack.
Another important thing to note is that, while our source code will be located inside ./src/web/frontendproj, the compiled output will be added to ./dist/web/frontendproj.
Let's add a new file ./src/web/index.ts, as is below:
export * from "./web";
Finally, we'll add an entry point - ./src/main.ts:
import {SERVER_PORT} from "./config";
import * as web from "./web";
async function main() {
await web.start(SERVER_PORT);
console.log(`Server up at port : http://localhost:${SERVER_PORT}`);
}
main().catch(error => console.error("Error : " + error));
That should complete our backend code. We should now be able to compile our code.
After adding the script for building the backend and express/ejs related packages, our package.json should look ilke below:
{
"name": "my-sample-react-ts-webpack",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: No tests specified\" && exit 1",
"build:backend": "tsc",
"start": "node ./dist/main.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/express": "^4.16.1",
"@types/node": "^11.9.6",
"typescript": "^3.3.3333"
},
"dependencies": {
"ejs": "^2.6.1",
"express": "^4.16.4"
}
}
If we hit [https://localhots:4200] in our browser, we should be able to see our page load without much content. Let's add some API code to our app. We'll add a new file MySampleController and add a single API, for now.
import { OK, BAD_REQUEST } from 'http-status-codes';
import { Controller, Get } from '@overnightjs/core';
import { Logger } from '@overnightjs/logger';
import { Request, Response } from 'express';
@Controller('api/my-sample-react-app')
class MySampleController {
public static readonly MSG = 'hello ';
@Get(':api_name')
private sayHello(req: Request, res: Response) {
try {
const { api_name } = req.params;
if (api_name === 'error-api') {
throw Error('There was some failure!');
}
Logger.Info(MySampleController.MSG + name);
return res.status(OK).json({
message: MySampleController.MSG + name,
});
} catch (err) {
Logger.Err(err, true);
return res.status(BAD_REQUEST).json({
error: err.message,
});
}
}
}
export default MySampleController;
In our server file, we'll import and trigger the controller we created above. Our code will look like:
import * as path from 'path';
import * as express from 'express';
import * as bodyParser from 'body-parser';
import * as MyControllers from './controllers';
class MyServer extends Server {
private readonly SERVER_STARTED_MSG = 'My server started on port: ';
private readonly DEV_RUNNING_MSG = 'Express Server is running in development mode ' +
'Content is not being served yet';
constructor() {
super(true);
this.app.use(bodyParser.json());
this.app.use(bodyParser.urlencoded({extended: true}));
super.addControllers(new DemoController());
// frontend code
if (process.env.NODE_ENV !== 'production') {
cinfo('Starting server in development mode');
const msg = this.DEV_RUNNING_MSG + process.env.EXPRESS_PORT;
this.app.get('*', (req, res) => res.send(msg));
}
}
private setupControllers(): void {
const controllerInstances = [];
for (const name in MyControllers) {
if (MyControllers.hasOwnProperty(name)) {
let Controller = (MyControllers as any)[name];
controllerInstances.push(new Controller());
}
}
super.addControllers(controllerInstances);
}
public start(port: number): void {
this.app.listen(port, () => {
Logger.Imp(this.SERVER_STARTED_MSG + port);
});
}
}
export default MyServer;
Conclusion
The steps above cover the various aspects in setting up any React project using Webpack and TypeScript. Using these plugins allows us greater control over our app configuration.