React with Typescript
This is a seed project on React with Typescript that I use for the majority of my projects.
As a veteran software engineer, I generally don't like using tools such as create-react-app. I prefer to create my project from scratch and want control over each of the tools and configuration files. As the web development industry changes, with cool things being created each day, I keep my seed projects up-to-date and stable so they can easily be cloned for rapid development.
At a glance
Markup (HTML) | JSX (React 18) |
Script | Typescript/React |
Styling (CSS) | Tailwind |
Build | Webpack 5/Terser |
Unit Test | Jest/Testing library |
Code quality/Linting: | Eslint |
Step 1: Create project and install basic packages
We will start by creating a directory and initialize npm.
npm init
This creates package.json. Let's add some packages.
// Basics
yarn add @types/react @types/react-dom
yarn add --dev typescript ts-loader ts-node
// Add test
yarn add --dev jest @types/jest @testing-library/react @testing-library/jest-dom
// Add dev tools/build
yarn add --dev webpack webpack-cli webpack-dev-server terser-webpack-plugin
yarn add --dev copy-webpack-plugin react-dom style-loader css-loader postcss-loader
@types/* are type declaration pacakges. testing-library is one of the popular testing packages out there for React. ts-node is the typescript exection engine for Node.
Time to create typescript configuration file.
{
"compilerOptions": {
"jsx": "react-jsx" /* This allows our JSX to be transpiled correctly */,
"target": "es6" /* Set the JS language version for emitted JavaScript */,
"esModuleInterop": true /* Emit additional JS to ease support for importing CommonJS modules.*/,
"allowSyntheticDefaultImports": true /* Need for Webpack config file */,
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
"strict": true /* Enable all strict type-checking options. */,
"skipLibCheck": true /* Skip type checking all .d.ts files. */,
"outDir": "dist",
"moduleResolution": "node",
"types": ["@testing-library/jest-dom"]
},
"include": ["**/*.ts", "**/*.tsx"]
}
Step 2: Setup webpack and react
We'll start by creating webpack configuration file - webpack.config.ts
style-loader extracts css and injects them to head element in a html document. css-loader resolves the dependencies such as import and url. postcss-loader makes css cool with linting, vendor prefix etc.
import path from "path";
import { Configuration } from "webpack";
import CopyWebpackPlugin from "copy-webpack-plugin";
import TerserPlugin from 'terser-webpack-plugin';
const config: Configuration = {
mode:
(process.env.NODE_ENV as "production" | "development" | undefined) ??
"development",
entry: "./src/index.tsx",
module: {
rules: [
{
test: /.tsx?$/,
use: [{
loader: 'ts-loader'
}],
exclude: /node_modules/,
},
{
test: /.css$/,
use: ["style-loader", "css-loader","postcss-loader"],
},
],
},
optimization: {
minimizer: [new TerserPlugin({
extractComments: false,
})],
},
resolve: {
extensions: [".tsx", ".ts", ".js"],
},
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist"),
},
plugins: [
new CopyWebpackPlugin({
patterns: [{ from: "public" }],
})
],
};
export default config;
};
Create entry file - index.jsx
Since React 18, createRoot is used instead of render API.
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
const rootElement = document.getElementById("root") as HTMLElement;
const root = createRoot(rootElement);
type AppProps = {
title?: string;
};
const App = ({ title }: AppProps) =>
<div className="elative bg-white px-6 pt-10 pb-8">
<h1 className="text-32l font-bold">{title}</h1>
</div>;
root.render(
<StrictMode>
<App title="Hello!!!"/>
</StrictMode>
);
Setup tailwind config file - tailwind.config.ts
Tailwind is kinda similar to bootstrap with its utility classes. But it doesn't provide classes for components like buttons and error messages.
module.exports = {
content: [
"./public/**/*.html",
"./src/**/*.tsx",
"./src/**/*.css"
],
theme: {
extend: {},
},
plugins: [],
}
Enable tailwind - tailwind.config.ts
@tailwind base;
@tailwind components;
@tailwind utilities;
Add HTML file - index.html
bundle.js is our bundled file (output).
<!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>Document</title>
</head>
<body>
<div id="root"></div>
<script src="bundle.js"></script>
</body>
<html></html>
</html>
Step 3: Add test
Add Test - jest.config.ts
Our jsx needs babel processing.
import type { Config } from 'jest';
const config: Config = {
clearMocks: true,
collectCoverage: true,
coverageDirectory: "coverage",
testEnvironment: "jsdom",
transform: {
'^.+\.tsx?$': [
'ts-jest',
{
babelConfig: true,
},
],
},
setupFilesAfterEnv: [
"<rootDir>/setupJest.ts"
],
};
export default config;
Create file - setupTest.ts
jest-dom extends dom with methods such as toContainHTML, toHaveClass etc.
import "@testing-library/jest-dom";
Step 4: Setup ESLint and finishing touches.
Create eslint configuration - .eslintrc.json
npm init @eslint/config
We'll use the popular airbnb styleguide - eslint-config-airbnb-typescript for our application. Package:
Update scripts in package.json
"dev": "webpack serve",
"build": "webpack",
"server": "nodemon",
"lint": "eslint src/**/*.tsx",
"test": "jest",
"coverage": "jest --coverage"
Step 5: Running the app.
npm run dev should start our dev server, npm run jest should start our unit test etc.