Creating a new web application in TypeScript these days involves a lot of boilerplate and setup of various tools, which takes up lots of time and energy that could be better spent developing the product itself.
For a long time, there was create-react-app, and life was good. Creating a project skeleton to build on top of was as easy as npx create-react-app my-cool-web-app
, but these days, create-react-app is no longer maintained and is outdated.
With this in mind, I created a template for my web app projects, offering all the usual tools and sane defaults & configurations with minimal bloat. It supports TypeScript, tests with Jest, builds with Webpack, automatically formats sources with Prettier, and lints with ESLint.
I named it react-ts-template, and this blog post will describe how I created it and explain how it works step-by-step. I hope this helps you learn why each component is required and how it fits in the application development environment.
The GitHub repo for this project can be found here: f3rno64/react-ts-template
With that in mind, let's get started!
I will be using yarn instead of npm throughout this post, and within the template itself. This is a matter of personal preference, and you can choose to stick with npm if you wish.q
Initial Setup
First, make a directory for your project and create a Git repository within it. Add a .gitignore
in the root directory with the following contents, which account for the most common environments:
/node_modules
/coverage
/build
/dist
/.pnp
/.yarn/cache
.env
.idea
.pnp.js
.DS_Store
.eslintcache
.vscode/settings.json
npm-debug.log*
yarn-debug.log*
yarn-error.log*
todo
notes
react-app-env.d.ts
tsconfig.tsbuildinfo
Note that the
tsconfig.tsbuildinfo
implies we will enable incremental TypeScript builds
Now, run yarn init
in the repo, provide the information it requests to initialize your package manifest. Next, set the yarn version to berry with yarn set version berry
to benefit from significant performance improvements relative to the stable yarn version.
Create a LICENSE.md in the project directory and populate it with your chosen license. I recommend the MIT license, which I will reproduce below:
The MIT License (MIT)
Copyright (c) 2015 bitfinexcom
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Tooling
Now, we'll go ahead and set up the various tools we will be using and their configurations. This section is fairly lengthy as I wish to explain the need behind each component for those new to web app development.
React
Install react
and react-dom
along with their type definitions by running yarn add react react-dom
and yarn add -D @types/react @types/react-dom
. That's all we need for React for now; dependencies related to testing will be covered in a later section.
TypeScript
Install @types/node
, typescript
and ts-node
with yarn add -D @types/node typescript ts-node
. This provides the TypeScript compiler, which Webpack will use to compile our source code to JavaScript.
Then, create the configuration file typescript.json
with the following contents:
{
"include": ["**/*.ts", "**/*.tsx"],
"compilerOptions": {
"jsx": "react-jsx",
"target": "es6",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"sourceMap": true,
"incremental": true,
"declaration": true,
"skipLibCheck": true,
"outDir": "dist",
"moduleResolution": "node"
}
}
This relatively lean configuration could be easily expanded for a stricter build process. Feel free to tweak it to your needs; the documentation can be found here.
Webpack
Webpack is a tool we will be using to build all our source files into a compiled bundle optimized for production. It offers many features and configuration options, which you can read about here.
Webpack requires several dependencies for the loaders and plugins we will be using. Install them all with the following command: yarn add -D webpack webpack-cli webpack-dev-server copy-webpack-plugin ts-loader style-loader sass sass-loader css-loader
.
Their roles are described below:
webpack
andwebpack-cli
are responsible for building the bundled app for deployment andwebpack-dev-server
provides a development server with live-reload.copy-webpack-plugin
will be used to copy the/public
directory (containing index.html and other static files) to/dist
as they are separate from our source files and not built by Webpack.ts-loader
,style-loader
,sass
,sass-loader
,css-loader
are all loaders that Webpack will use to process and build our source files. They provide support for TypeScript and SASS stylesheets. PostCSS will be described separately in a later section.
Finally, let's create the configuration file for Webpack, aptly named webpack.config.ts
with the following contents:
/* eslint-disable */
const path = require('path')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const { NODE_ENV } = process.env
const config = {
mode: NODE_ENV ?? 'development',
entry: path.resolve(__dirname, 'src/index.tsx'),
module: {
rules: [
{
test: /.tsx?$/,
exclude: /node_modules/,
use: 'ts-loader'
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader']
},
{
test: /\.s[ac]ss$/i,
use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader']
}
]
},
resolve: {
extensions: ['.tsx', '.ts', '.js']
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
new CopyWebpackPlugin({
patterns: [{ from: 'public' }]
})
]
}
module.exports = config
Note that it defines the entry point as src/index.tsx
; this file will be responsible for rendering our React application within the #root
element present in index.html
. The rest of the configuration is self-explanatory, mainly defining rules for loaders to process specific files by their extensions.
One thing to note is the inclusion of the
CopyWebpackPlugin
, which is responsible for copying the contents ofpublic/
intodist/
.
This configuration should be expanded to fit your needs; it simply serves as a starting point.
PostCSS
PostCSS is a powerful tool providing quality-of-life features for working with CSS. It is included in the configuration with minimal plugins to offer a small but helpful starting point.
Install it, it's plugins, and it's Webpack loader with yarn add -D postcss postcss-flexbugs-fixes postcss-normalize postcss-preset-env postcss-loader
.
Create its config file, postcss.config.js
and populate it with the following contents to enable the installed plugins:
module.exports = {
plugins: [
['postcss-preset-env', {}],
['postcss-normalize', {}],
['postcss-flexbugs-fixes', {}]
]
}
Browsers List
We need to add a section to package.json
that describes the browsers our web app supports in development and production environments. Various tools use this to infer feature compatibility. Add the following JSON block to package.json
:
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
Prettier
Prettier is a tool that automatically formats source files to maintain a consistent style. I use it in my editor (neovim) to format whenever I save a file, but for this project, it will be accessible via a format
script in the manifest.
Install with yarn add -D prettier
and create the .prettierrc.json
config file with the following contents:
{
"trailingComma": "none",
"tabWidth": 2,
"semi": false,
"singleQuote": true
}
Feel free to expand this configuration to your liking.
ESLint
We will use ESLint to check our sources for poor style or other inconsistencies. It is integrated into most code editors and runs as part of the testing process. I've included many plugins to provide a fairly strict configuration.
Install it, and it's presets and plugins with yarn add -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-config-standard-with-typescript eslint-plugin-import eslint-plugin-react-hooks eslint-plugin-promise eslint-plugin-react eslint-plugin-jsx-a11y eslint-plugin-jest
Then, create the configuration file .eslintrc.js
and populate it with the following contents to make use of all those presets and plugins:
/* eslint-env node */
module.exports = {
root: true,
env: {
browser: true,
es2022: true
},
plugins: ['@typescript-eslint', 'promise', 'jsx-a11y', 'jest', 'react'],
extends: [
'eslint:recommended',
'standard-with-typescript',
'plugin:@typescript-eslint/recommended',
'plugin:import/recommended',
'plugin:import/typescript',
'plugin:react-hooks/recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:jsx-a11y/recommended'
],
settings: {
'import/resolver': {
typescript: true,
node: {
extensions: ['.js', '.jsx', '.ts', '.tsx']
}
},
react: {
version: 'detect'
}
},
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json'],
ecmaFeatures: {
jsx: true
}
},
rules: {
'import/no-named-as-default-member': 0
}
}
Feel free to remove any plugins or presets depending on your needs or add more. You can also configure or turn off individual rules as your needs require.
Jest
Jest is the test runner we will be using to test the application components, together with @testing-library/react
& it's related dependencies.
Install it with yarn add -D @types/jest jest jest-environment-jsdom ts-jest @testing-library/dom @test-library/jsdom @testing-library/react @testing-library/react-hooks @testing-library/user-event
Then create the jest.config.ts
configuration file with the following contents:
export default {
clearMocks: true,
preset: 'ts-jest/presets/default-esm',
coverageDirectory: '<rootDir>/coverage',
collectCoverageFrom: [
'**/*.{js,jsx,ts,tsx}',
'!**/node_modules/**',
'!**/coverage/**',
'!**/public/**',
'!**/mocks/**',
'!**/dist/**',
'!webpack.config.js',
'!postcss.config.js',
'!.eslintrc.js'
],
transform: {
'\\.(js|jsx|ts|tsx)$': [
'ts-jest',
{
tsconfig: '<rootDir>/tsconfig.json',
useESM: true,
diagnostics: true
}
]
},
moduleNameMapper: {
'\\.(css|less|sass|scss)$': '<rootDir>/mocks/style.js'
}
}
The only thing to note here is the moduleNameMapper
entry, which maps all stylesheets to an empty style.js
file so Jest doesn't import stylesheets when they are imported within a component under test.
Create the mocks
directory and add style.js
within it with the following contents:
module.exports = {}
Husky
We will use Husky to configure a pre-commit hook that will run our lint and test scripts. Install it with yarn add -D husky
.
Then run the following commands to set the pre-commit hook:
npx husky add .husky/pre-commit "yarn lint && yarn test"
git add .husky/pre-commit
We will setup the
prepare
script in the next section
Manifest Scripts
Before we add the scripts
block, install some utilities which we will be using with yarn add -D cross-env open-cli standard-version
.
Then add the following scripts
block to package.json
:
"scripts": {
"start": "open-cli http://localhost:8080 && webpack serve",
"build": "NODE_ENV=production webpack --progress --fail-on-warnings",
"format": "prettier -w src/**",
"lint": "eslint --ext .js,.jsx,.ts,.tsx src",
"test": "cross-env NODE_OPTIONS='--experimental-vm-modules --no-warnings' npx jest",
"test:coverage": "cross-env NODE_OPTIONS='--experimental-vm-modules --no-warnings' npx jest --coverage",
"test:watch": "cross-env NODE_OPTIONS='--experimental-vm-modules --no-warnings' npx jest --watch",
"test:snapshots": "cross-env NODE_OPTIONS='--experimental-vm-modules --no-warnings' npx jest -u",
"update-version": "standard-version -a",
"prepare": "husky install",
"prepare-release": "npm run lint && npm test && npm run build",
"release": "npm run prepare-release && npm run update-version"
}
I will go ahead and describe each script below:
start - opens the application in a browser and runs the Webpack server
build - generates the production-ready build of the app in
dist/
format - runs Prettier on all source files, formatting them for a consistent style
lint - runs ESLint and prints any errors & warnings
test - runs all tests in the source folder with Jest
test:coverage - like test but generates test coverage information in
coverage/
test:watch - watches for changes to sources and runs tests when they change
test:snapshots - runs tests and updates Jest test snapshots
update-version - updates all dependencies to their latest version
prepare - installs the Husky pre-commit hooks
prepare-release - lints, tests, and builds the application
release - lints, tests, builds, bumps the version number, commits, and tags it
The Code Itself
Finally, it's time to start writing some code! We'll go through the structure of components & pages and create a basic component to serve as a starting point. I've purposefully left out routing for the time being to keep things simple.
Public Directory
Now it's time to make the public
directory in the project root; this will contain static files such as index.html
the page that will host our app, and other files like fonts, images, etc. It will be copied into the dist
directory as part of the build process.
Created it with mkdir public
and populate public/index.html
with the following contents:
<!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>Project Title</title>
</head>
<body>
<div id="root"></div>
<script src="bundle.js"></script>
</body>
<html></html>
</html>
Project Sources
Finally, it's time actually to develop our application! Go ahead and create the src
directory with mkdir src
.
We need two new dependencies, prop-types
for defining React component prop types, and classnames
which is an excellent utility for compiling multiple class names into a single string. Install them with yarn add prop-types classnames
.
Let's start with the root stylesheet, index.scss
, which will import normalize.css, a CSS library that provides a consistent set of base styles for all browser environments. Install it with yarn add normalize.css
. Now create and populate src/index.scss
with the following content:
@import '~normalize.css';
html, body, #root {
height: 100%;
}
Project Components
The structure we will follow here for our application components and pages is one that I have used in both professional and personal projects. It is clean and scales well. Each component resides inside its directory within src/components
, and each page within src/pages
.
Go ahead and make these directories with mkdir src/components
and mkdir src/pages
.
All components and pages are made up of the following five files:
const.ts
- contains constants, such asCLASS_NAME
, used by the componentstyle.scss
- the component stylesheet, imported inindex.tsx
types.ts
- component type & interface definitions, such asComponentProps
props.ts
- exports componentpropTypes
anddefaultProps
objectsindex.tsx
- the component itself, importing everything needed from the other files
Both src/pages
and src/components
should contain an index.ts
file that imports all sub-directories and exports them together so they can be imported easily, like import * as C from './components'
.
React Root
To create the entry point for our application, make and populate a src/index.tsx
file with the following contents:
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import * as C from './components'
import './index.scss'
const rootElement = document.getElementById('root')
if (rootElement === null) {
throw new Error('Root element not found')
}
const root = createRoot(rootElement)
root.render(
<StrictMode>
<C.UI />
</StrictMode>
)
This renders the UI component, which will be defined in the next section, into the #root
element of our index.html
page.
UI Component
I usually name the root application component UI, and, within it import all of the pages from src/pages
and set up routing. For this template, it will simply render a Home page and expose a prop to allow for a custom class name.
I usually expose a className prop on all of my components, as a standard practice.
Create a src/components/ui
folder, and populate src/components/ui/index.tsx
with the following contents:
import type React from 'react'
import classNames from 'classnames'
import * as P from '../../pages'
import { CLASS_NAME } from './const'
import { type UIProps } from './types'
import { propTypes, defaultProps } from './props'
import './style.scss'
const UI: React.FC<UIProps> = (props: UIProps) => {
const { className } = props
const finalClassName = classNames(CLASS_NAME, className)
return (
<div className={finalClassName}>
<P.Home />
</div>
)
}
UI.propTypes = propTypes
UI.defaultProps = defaultProps
export default UI
export { CLASS_NAME, type UIProps }
Similarly, create and populate src/components/ui/const.ts
with the following contents:
export const CLASS_NAME = 'component-ui'
I always prefix component class names with
component
, and page class names withpage
.
Do the same for src/components/ui/props.ts
:
import PropTypes from 'prop-types'
export const propTypes = {
className: PropTypes.string
}
export const defaultProps = {}
And src/components/ui/types.ts
:
export interface UIProps {
className?: string
}
And finally, the stylesheet src/components/ui/style.scss
:
.component-ui {
height: 100%;
}
Don't forget to add the component to src/components/index.ts
so it can be easily imported elsewhere. The contents of the file should be:
import UI from './ui'
export { UI }
UI Component Tests
In an ideal world, all components and tests should be thoroughly tested, at least with unit tests. For now, we will validate the component against a snapshot to notice any changes in the future and verify any custom class name is applied. Create a src/components/ui/__tests__
folder and populate src/components/ui/__tests__/ui.test.tsx
with the following contents:
/**
* @jest-environment jsdom
*/
import '@testing-library/jest-dom'
import { render } from '@testing-library/react'
import UI from '../'
describe('components:ui', () => {
it('matches snapshot with default test props', async () => {
const { container } = render(<UI />)
expect(container).toMatchSnapshot()
})
it('uses the provided class name', async () => {
const className = 'test-class'
const { container } = render(<UI className={className} />)
expect(container).toMatchSnapshot()
expect(container.firstChild).toHaveClass(className)
})
})
Home Page
Finally, let's create the page that our UI component renders. As this is an application template, the Home page will only render a vertically & horizontally centered message to show that everything is working as it should.
To keep things short, I'll simply list the files below without much explanation. The internals are similar to the UI component, as the page doesn't do much.
First, we have src/pages/home/index.tsx
:
import type React from 'react'
import classNames from 'classnames'
import { CLASS_NAME } from './const'
import { type HomeProps } from './types'
import { propTypes, defaultProps } from './props'
import './style.scss'
const Home: React.FC<HomeProps> = (props: HomeProps) => {
const { className } = props
const finalClassName = classNames(CLASS_NAME, className)
return (
<div className={finalClassName}>
<div className={`${CLASS_NAME}-content-wrapper`}>
<h2>React TypeScript Web App Template</h2>
</div>
</div>
)
}
Home.propTypes = propTypes
Home.defaultProps = defaultProps
export default Home
export { CLASS_NAME, type HomeProps }
Then src/pages/home/const.ts
:
export const CLASS_NAME = 'page-home'
Also src/pages/home/props.ts
:
import PropTypes from 'prop-types'
export const propTypes = {
className: PropTypes.string
}
export const defaultProps = {}
And src/pages/home/types.ts
:
export interface HomeProps {
className?: string
}
Then finally src/pages/home/style.scss
:
.page-home {
height: 100%;
.page-home-content-wrapper {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #eee;
h2 {
color: #000;
}
}
}
Also, as before, include the page in src/pages/index.ts
for easy importing elsewhere:
import Home from './home'
export { Home }
Home Page Tests
We will create a basic file with unit tests as with the UI component. The only difference from the UI component tests is that we will verify the header text is rendered. Create a src/pages/home/___tests__
folder and populate src/pages/home/__tests__/home.test.tsx
with the following content:
/**
* @jest-environment jsdom
*/
import '@testing-library/jest-dom'
import { render } from '@testing-library/react'
import Home from '../'
describe('pages:home', () => {
it('matches snapshot with default test props', async () => {
const { container } = render(<Home />)
expect(container).toMatchSnapshot()
})
it('uses the provided class name', async () => {
const className = 'test-class'
const { container } = render(<Home className={className} />)
expect(container).toMatchSnapshot()
expect(container.firstChild).toHaveClass(className)
})
it('renders the template header text', async () => {
const { container } = render(<Home />)
expect(container).toMatchSnapshot()
const header = container.querySelector('h2')
expect(header).toHaveTextContent('React TypeScript Web App Template')
})
})
Conclusion
Finally, we are done! I hope you've learned something from my explanation of the construction of this template.
To use it, fork it on GitHub and start filling in the internals for your application and its needs. Feel free to tweak the various config files to your liking.