RETROSPECTIVE

November 15th, 2021

Building a Web Application with React and TypeScript

React

TypeScript

JavaScript

Webpack

Babel

ESLint

Prettier

s

This is part of a series of articles on SaintsXCTF Version 2.0. The first article in the series provides an overview of the application. You DO NOT need to read prior articles in the series to fully understand this article.

I recently rewrote my web application saintsxctf.com using React and TypeScript. This article looks at how the application is configured and walks through some React code. All the code for the website exists in a GitHub repository.

The TypeScript React application for SaintsXCTF is bundled with Webpack and transpiled with Babel. My React application is not configured with create-react-app. This approach requires more time and knowledge of React, Webpack, TypeScript, and Babel from the engineer, with the benefit of increased customization. The application is also an npm package, and therefore has a package.json configuration file. Note that both Typescript and React are specified as application dependencies.

TypeScript is configured for the application in a tsconfig.json file. The TypeScript configuration for SaintsXCTF is shown below.

{ "compilerOptions": { "outDir": "./dist/", "sourceMap": true, "declaration": false, "noImplicitAny": true, "module": "ES6", "moduleResolution": "Node", "esModuleInterop": true, "target": "ES2020", "jsx": "react" }, "exclude": ["node_modules", "test", "cypress"] }

tsconfig.json signifies the root directory of the TypeScript application and sets certain configuration parameters. Most of these fall under compilerOptions, which determines how the TypeScript language works (how it compiles)1. Webpack, specifically ts-loader, uses the tsconfig.json file to determine how to compile TypeScript files2. Behind the scenes, ts-loader uses the TypeScript compiler tsc.

Let’s look through the configuration properties within compilerOptions. "outDir": "./dist/" specifies that JavaScript files compiled from TypeScript code are placed in a dist directory within the repository. "sourceMap": true enables source maps, which are useful for debugging 3. "declaration": false specifies that JavaScript files created from TypeScript code don’t require corresponding type definition (*.d.ts) files4. This is because the codebase isn’t used as a third party library; instead, it is read by a web browser when users navigate to the website. "noImplicitAny": true helps enforce type safety. "module": "ES6" sets the JavaScript module system to use for compiled code. "target": "ES2020" sets the target JavaScript version of the compiled JavaScript code, and "jsx": "react" converts all JSX code to the equivalent JavaScript React library functions.

The final piece of the TypeScript configuration file is exclude, which specifies that TypeScript files in certain directories are not part of the TypeScript program. Excluded directories are external dependencies (node_modules), unit tests (test), and end to end tests (cypress).

As I mentioned, the configuration in tsconfig.json is used by Webpack, specifically the loader ts-loader. Note that ts-loader is also specified as a dependency in package.json. My application's Webpack configuration is specified in two files: webpack.config.js and webpack.parts.js. Inside webpack.config.js, ts-loader is configured to transpile TypeScript code to JavaScript code with the following setup:

{ test: /\.ts(x?)$/, exclude: /(node_modules)/, use: [ { loader: 'babel-loader' }, { loader: 'ts-loader' } ] }

This code configures Webpack loaders to be applied to certain file patterns. The file pattern specified is the regular expression /\.ts(x?)$/, matching Typescript files with .ts and .tsx file extensions. Webpack loaders preprocess files by performing transformations on code5,6. This step is executed prior to Webpack bundling the code. The two loaders specified to execute on TypeScript files are babel-loader and ts-loader.

With this setup, ts-loader is run on the TypeScript code first, followed by babel-loader7. Babel is a JavaScript compiler. In my configuration, babel-loader further compiles the intermediary JavaScript code that is created by ts-loader8. Babel is configured by a .babelrc file, which is located at the root directory of the repository alongside the Webpack configuration.

This process of compiling and bundling code is performed for my production and development environments, which are accessible from saintsxctf.com and dev.saintsxctf.com, respectively. In my local environment, webpack-dev-server is used to host the application. webpack-dev-server runs an application server for the React application and enables hot reloading of the webpage. This means that whenever TypeScript files in the application are updated, the web application automatically reloads with the latest changes. This makes development a breeze! webpack-dev-server is configured in my webpack.config.js file and is run from a terminal with the webpack-dev-server --hot --inline --env local command. Its configuration is shown below.

devServer: { historyApiFallback: true, port: 8090, proxy: { '/api/**': { target: 'http://localhost:5000/', secure: false, pathRewrite: { '^/api': '' } }, '/auth/**': { target: 'http://localhost:5001/', secure: false, pathRewrite: { '^/auth': '' } }, '/fn/**': { target: 'https://fn.saintsxctf.com/', secure: true, changeOrigin: true, pathRewrite: { '^/fn': '' } }, '/asset/**': { target: 'https://asset.saintsxctf.com/', secure: true, changeOrigin: true, pathRewrite: { '^/asset': '' } }, '/uasset/**': { target: 'https://uasset.saintsxctf.com/dev/', secure: true, changeOrigin: true, pathRewrite: { '^/uasset': '' } }, '/s3/**': { target: 'https://s3.amazonaws.com/', secure: true, changeOrigin: true, pathRewrite: { '^/s3': '' } }, } }

This configuration specifies that the local development server for the application runs on port 8090. It also creates a reverse proxy server for API requests, which is configured under the proxy object. In development and production, the reverse proxy server is created with Nginx, and is configured in a nginx.conf file. I wrote an article on Nginx reverse proxy servers if you want to learn more.

The final piece of configuration is an .eslintrc.js file for ESLint and Prettier. ESLint is a linter which analyzes JavaScript and TypeScript code and detects any issues or formatting mistakes. Engineers are able to set which rules they want tested, such as the maximum number of characters on a line and whether to use single quotes or double quotes for strings. Prettier is a code formatter which can operate with ESLint. Therefore, Prettier can automatically reformat code to match all the ESLint rules.

By default, ESLint analyzes JavaScript code. However, a @typescript-eslint/eslint-parser npm package can be installed, allowing ESLint to analyze TypeScript code. TypeScript also has its own set of default linting rules, which are found in a @typescript-eslint/eslint-plugin npm package. The ESLint configuration for my application is shown below. You can adapt it to your application needs and code formatting preferences.

module.exports = { parser: "@typescript-eslint/parser", extends: [ 'plugin:react/recommended', 'plugin:@typescript-eslint/recommended', 'prettier/@typescript-eslint' ], parserOptions: { ecmaVersion: 2020, sourceType: 'module', ecmaFeatures: { jsx: true } }, settings: { react: { version: 'detect' } }, plugins: [ "prettier", "react-hooks" ], rules: { "max-len": ["error", { "code": 120 }], "quotes": ["error", "single", { "avoidEscape": true }], "react/prop-types": ["off"], "react/no-unescaped-entities": ["off"], "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "warn", "@typescript-eslint/camelcase": ["off"], "prettier/prettier": ["error", { "singleQuote": true, "printWidth": 120, "trailingComma": "none", }] }, ignorePatterns: ['webpack.config.js', 'webpack.parts.js', 'jest.config.js'] };

React code for my SaintsXCTF application exists in a src directory. The codebase has the following folder structure:

index.html is the entrypoint to the web application, and index.tsx bootstraps the React application.

The subdirectories inside src contain TypeScript application code. components contains React components used by the application. containers also contains React components, however these are top-level components, one for each page in the application. datasources configures axios instances for each API used by the application9. These instances determine how HTTP requests are routed and define actions to take on requests and responses. hooks contains custom React hooks which are reused throughout the application. redux configures Redux as the state management system for the application. styles contains global JSS styles and variables which are reused in component stylesheets. Finally, utils contains reusable TypeScript functions which are shared amongst components.

Let’s look at some React components written in TypeScript. One of the basic components in my application is a custom checkbox. The checkbox component, named CheckBox, is shown below.

import React from 'react'; import { createUseStyles } from 'react-jss'; import styles from './styles'; import classNames from 'classnames'; import { ClassValue } from 'classnames/types'; interface Props { id?: string; checked: boolean; onChange: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void; className?: ClassValue; } const useStyles = createUseStyles(styles); const CheckBox: React.FunctionComponent<Props> = ({ id, checked, onChange, className }) => { const classes = useStyles(); return ( <div className={classNames(classes.checkBox, className)} onClick={onChange}> <input type="checkbox" id={id} className={classes.input} checked={checked} /> <span>{checked && <p>N</p>}</span> </div> ); }; export default CheckBox;

CheckBox is a straightforward functional React component. In this article, I assume that you understand basics concepts of the React library. However, I will go over aspects of the code which are specific to TypeScript. First, let’s consider how a JavaScript equivalent of CheckBox is written.

import React from 'react'; import { createUseStyles } from 'react-jss'; import styles from './styles'; import classNames from 'classnames'; import PropTypes from 'prop-types'; const useStyles = createUseStyles(styles); const CheckBox = ({ id, checked, onChange, className }) => { const classes = useStyles(); return ( <div className={classNames(classes.checkBox, className)} onClick={onChange}> <input type="checkbox" id={id} className={classes.input} checked={checked} /> <span>{checked && <p>N</p>}</span> </div> ); }; CheckBox.propTypes = { id: PropTypes.string, checked: PropTypes.bool.isRequired, onChange: PropTypes.func.isRequired, className: PropTypes.string }; export default CheckBox;

The JavaScript and TypeScript versions of CheckBox are very similar. The differences relate to type definitions. For example, in TypeScript, CheckBox is given the type React.FunctionComponent<Props>, while the JavaScript code does not supply an explicit type. In TypeScript, Props is also an explicitly defined type, created as an interface. In JavaScript, types of props are specified using the CheckBox.propTypes syntax with assistance from the PropTypes library.

Functions defined within components also take advantage of TypeScript’s explicit type definitions. For example, I have a component named UploadFile which allows users to upload pictures. It responds to DOM events such as page clicks or files being dragged and dropped. The full component code is on GitHub, with the DOM event listeners shown below.

const handleDragEnter = (e: React.DragEvent<HTMLDivElement>): void => { ... }; const handleDragLeave = (e: React.DragEvent<HTMLDivElement>): void => { ... }; const handleDragOver = (e: React.DragEvent<HTMLDivElement>): void => { ... }; const handleDrop = (e: React.DragEvent<HTMLDivElement>): void => { ... }; const handleClickUpload = (): void => { ... }; const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>): void => { ... };

Event listener argument type definitions such as React.DragEvent<HTMLDivElement> or React.ChangeEvent<HTMLInputElement> are unique to TypeScript and do not exist and are not enforced in JavaScript code. The event listener return types, in my case void, are also not enforced or explicitly written in JavaScript.

As the code shows, the biggest benefit of using TypeScript in React applications is type safety, specifically static type analysis. With TypeScript, subtle runtime type errors such as calling string methods on number types are avoided; instead, these errors are caught during code compilation. Configuring a React application with TypeScript is also relatively easy. Best of all, existing JavaScript applications can slowly be transformed into TypeScript, since both languages can coexist together. TypeScript code can import JavaScript code, and vice versa. You can view all the code for my SaintsXCTF React application on GitHub.

[1] "TSConfig Reference: compilerOptions", https://www.typescriptlang.org/tsconfig#compilerOptions

[2] "TypeScript: Loader", https://webpack.js.org/guides/typescript/#loader

[3] "TSConfig Reference: sourceMap", https://www.typescriptlang.org/tsconfig#sourceMap

[4] "TSConfig Reference: declaration", https://www.typescriptlang.org/tsconfig#declaration

[5] "Loaders", https://webpack.js.org/loaders/

[6] "Loaders", https://webpack.js.org/concepts/loaders/

[7] "What is the loader order for webpack?", https://stackoverflow.com/a/32234468

[8] "babel-loader", https://webpack.js.org/loaders/babel-loader/

[9] "The Axios Instance", https://axios-http.com/docs/instance