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.
Let's start by creating a new directory for our project.
1mkdir my-sample-react-ts-webpack
2cd my-sample-react-ts-webpack
We'll use npm to initialize our project as below:
1npm 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.
1npm install --save-dev webpack webpack-cli
2npm install --save react react-dom
3npm install --save-dev @types/react @types/react-dom
4npm 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":
Thus, our folder structure will look something like below:
1├── README.md
2├── package.json
3├── package-lock.json
4├── server.js
5├── tsconfig.json
6├── webpack.config.js
7├── .gitignore
8└── src
9 └──app
10 └──components
11 ├── App.tsx
12 ├── index.tsx
13 ├── HelloWorld.tsx
14 ├── index.html
We'll start with index.html.
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <meta http-equiv="X-UA-Compatible" content="ie=edge">
7 <title>Let's learn to create a React app using Typescript and Webpack</title>
8</head>
9<body>
10 <div id="content"></div>
11</body>
12</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.
1{
2 "compilerOptions": {
3 "jsx": "react",
4 "module": "commonjs",
5 "noImplicitAny": true,
6 "outDir": "./build/",
7 "preserveConstEnums": true,
8 "removeComments": true,
9 "sourceMap": true,
10 "target": "es5"
11 },
12 "include": [
13 "./src/**/**/*"
14 ]
15 }
Now, let's add some Webpack configuration to webpack.config.js.
1const path = require('path'),
2 webpack = require('webpack'),
3 HtmlWebpackPlugin = require('html-webpack-plugin');
4
5module.exports = {
6 entry: {
7 app: ['./src/app/App.tsx', 'webpack-hot-middleware/client'],
8 vendor: ['react', 'react-dom']
9 },
10 output: {
11 path: path.resolve(__dirname, 'dist'),
12 filename: 'js/[name].bundle.js'
13 },
14 devtool: 'source-map',
15 resolve: {
16 extensions: ['.js', '.jsx', '.json', '.ts', '.tsx']
17 },
18 module: {
19 rules: [
20 {
21 test: /\.(ts|tsx)$/,
22 loader: 'ts-loader'
23 },
24 { enforce: "pre", test: /\.js$/, loader: "source-map-loader" }
25 ]
26 },
27 plugins: [
28 new HtmlWebpackPlugin({ template: path.resolve(__dirname, 'src', 'app', 'index.html') }),
29 new webpack.HotModuleReplacementPlugin()
30 ]
31}
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:
1const path = require('path');
2
3const isProduction = typeof NODE_ENV !== 'undefined' && NODE_ENV === 'production';
4const mode = isProduction ? 'production' : 'development';
5const devtool = isProduction ? false : 'inline-source-map';
6module.exports = [
7 {
8 entry: './src/client.ts',
9 target: 'web',
10 mode,
11 devtool,
12 module: {
13 rules: [
14 {
15 test: /\.tsx?$/,
16 use: 'ts-loader',
17 exclude: /node_modules/,
18 options: {
19 compilerOptions: {
20 "sourceMap": !isProduction,
21 }
22 }
23 }
24 ]
25 },
26 resolve: {
27 extensions: [ '.tsx', '.ts', '.js' ]
28 },
29 output: {
30 filename: 'client.js',
31 path: path.join(__dirname, 'dist', 'public')
32 }
33 },
34 {
35 entry: './src/server.ts',
36 target: 'node',
37 mode,
38 devtool,
39 module: {
40 rules: [
41 {
42 test: /\.tsx?$/,
43 use: 'ts-loader',
44 exclude: /node_modules/
45 }
46 ]
47 },
48 resolve: {
49 extensions: [ '.tsx', '.ts', '.js' ]
50 },
51 output: {
52 filename: 'server.js',
53 path: path.resolve(__dirname, 'dist')
54 },
55 node: {
56 __dirname: false,
57 __filename: false,
58 }
59 }
60];
Our server.js should look like below:
1const path = require('path'),
2 express = require('express'),
3 webpack = require('webpack'),
4 webpackConfig = require('./webpack.config.js'),
5 app = express(),
6 port = process.env.PORT || 3000;app.listen(port, () => { console.log(`App is listening on port ${port}`) });app.get('/', (req, res) => {
7 res.sendFile(path.resolve(__dirname, 'dist', 'index.html'));
8});let compiler = webpack(webpackConfig);
9app.use(require('webpack-dev-middleware')(compiler, {
10 noInfo: true, publicPath: webpackConfig.output.publicPath, stats: { colors: true }
11}));
12app.use(require('webpack-hot-middleware')(compiler));
13app.use(express.static(path.resolve(__dirname, 'dist')));
Let's add the code to our React component HelloWorld.tsx:
1import * as React from "react";
2
3export interface HelloWorldProps { firstName: string; lastName: string; }
4
5export 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:
1import * as React from "react";
2
3export interface HelloWorldProps { firstName: string; lastName: string; }
4
5// 'HelloWorldProps' describes our props structure.
6// For the state, we use the '{}' type.
7export class HelloWorld extends React.Component<HelloWorldProps, {}> {
8 render() {
9 return <h1>Hi there from React! {this.props.firstName} and {this.props.lastName}!</h1>;
10 }
11}
Now, let's update the code in index.tsx as is shown below:
1import * as React from "react";
2import * as ReactDOM from "react-dom";
3
4import { HelloWorld } from "./components/HelloWorld";
5
6ReactDOM.render(
7 <HelloWorld firstName="Chris" lastName="Parker" />,
8 document.getElementById("content")
9);
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.
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.
1import { HelloWorld } from './components/HelloWorld';
Let's also have a look at the different options we added to tsconfig.json:
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.
1...
2scripts: {
3...
4 "build": "./node_modules/.bin/webpack",
5...
6}
7...
We can now go to the command prompt and run the following command:
1npm run build
We can add different scripts to build React apps in our package.json, as is shown below:
1...
2"start": "webpack-dev-server --open",
3"devbuild": "webpack --mode development",
4"build": "webpack --mode production"
5...
We can use Express as the framework for the backend with EJS as the templating engine. Let's install the same using:
1$ npm i express
2$ npm i -D @types/express
3$ npm i ejs
In our config.ts file, we'll add the following code for the server port:
1export 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:
1import express from "express";
2import http from "http";
3import path from "path";
4
5// Initialization
6const app = express();
7
8// Configuration for templating engine
9app.set("view engine", "ejs");
10app.set("views", "public");
11
12// Configuration for static files
13app.use("/assets", express.static(path.join(__dirname, "frontendproj")));
14
15// Add Controllers
16app.get("/*", (req, res) => {
17 res.render("index");
18});
19
20// Add start method
21export const start = (port: number): Promise<void> => {
22 const server = http.createServer(app);
23
24 return new Promise<void>((resolve, reject) => {
25 server.listen(port, resolve);
26 });
27};
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:
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <title>React with Typescript and Webpack with EJS Templating Engine</title>
5
6 <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
7 <meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
8
9</head>
10<body>
11 <div id="root"></div>
12
13 <script src="/assets/vendors.js"></script>
14 <script src="/assets/main.bundle.js"></script>
15</body>
16</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:
1export * from "./web";
Finally, we'll add an entry point - ./src/main.ts:
1import {SERVER_PORT} from "./config";
2
3import * as web from "./web";
4
5async function main() {
6 await web.start(SERVER_PORT);
7 console.log(`Server up at port : http://localhost:${SERVER_PORT}`);
8}
9
10main().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:
1{
2 "name": "my-sample-react-ts-webpack",
3 "version": "1.0.0",
4 "description": "",
5 "main": "index.js",
6 "scripts": {
7 "test": "echo \"Error: No tests specified\" && exit 1",
8 "build:backend": "tsc",
9 "start": "node ./dist/main.js"
10 },
11 "keywords": [],
12 "author": "",
13 "license": "ISC",
14 "devDependencies": {
15 "@types/express": "^4.16.1",
16 "@types/node": "^11.9.6",
17 "typescript": "^3.3.3333"
18 },
19 "dependencies": {
20 "ejs": "^2.6.1",
21 "express": "^4.16.4"
22 }
23}
If we hit http://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.
1import { OK, BAD_REQUEST } from 'http-status-codes';
2import { Controller, Get } from '@overnightjs/core';
3import { Logger } from '@overnightjs/logger';
4import { Request, Response } from 'express';
5
6@Controller('api/my-sample-react-app')
7class MySampleController {
8
9 public static readonly MSG = 'hello ';
10
11 @Get(':api_name')
12 private sayHello(req: Request, res: Response) {
13 try {
14 const { api_name } = req.params;
15 if (api_name === 'error-api') {
16 throw Error('There was some failure!');
17 }
18 Logger.Info(MySampleController.MSG + name);
19 return res.status(OK).json({
20 message: MySampleController.MSG + name,
21 });
22 } catch (err) {
23 Logger.Err(err, true);
24 return res.status(BAD_REQUEST).json({
25 error: err.message,
26 });
27 }
28 }
29}
30
31export default MySampleController;
In our server file, we'll import and trigger the controller we created above. Our code will look like:
1import * as path from 'path';
2import * as express from 'express';
3import * as bodyParser from 'body-parser';
4import * as MyControllers from './controllers';
5
6class MyServer extends Server {
7
8 private readonly SERVER_STARTED_MSG = 'My server started on port: ';
9 private readonly DEV_RUNNING_MSG = 'Express Server is running in development mode ' +
10 'Content is not being served yet';
11
12 constructor() {
13 super(true);
14 this.app.use(bodyParser.json());
15 this.app.use(bodyParser.urlencoded({extended: true}));
16 super.addControllers(new DemoController());
17 // frontend code
18 if (process.env.NODE_ENV !== 'production') {
19 cinfo('Starting server in development mode');
20 const msg = this.DEV_RUNNING_MSG + process.env.EXPRESS_PORT;
21 this.app.get('*', (req, res) => res.send(msg));
22 }
23 }
24
25 private setupControllers(): void {
26 const controllerInstances = [];
27 for (const name in MyControllers) {
28 if (MyControllers.hasOwnProperty(name)) {
29 let Controller = (MyControllers as any)[name];
30 controllerInstances.push(new Controller());
31 }
32 }
33 super.addControllers(controllerInstances);
34 }
35
36 public start(port: number): void {
37 this.app.listen(port, () => {
38 Logger.Imp(this.SERVER_STARTED_MSG + port);
39 });
40 }
41}
42
43export default MyServer;
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.