Create a React Typescript App FROM SCRATCH
Learn how to create a TypeScript React app from scratch using Webpack and Babel. We will not use create-react-app or vite.
Table of Contents 📖
- Project Initialization & Installing React
- Installing Typings
- Configuring TypeScript
- Installing Webpack
- Installing Babel Loader and Presets
- Handling HTML
- Configuring Webpack
- Handling TSX Code
- Handling CSS
- Handling CSS Modules
- Webpack and Assets
- Creating a globals.d.ts File
- Writing Some CSS
- Writing the React Code
- Adding a Server to Serve up the Application
- Running the Application
Project Initialization & Installing React
To begin, lets initialize our project as an ES6 npm project.
npm init es6 -y
Now lets install React. React is just a library, so we can install it with npm.
npm i react
We will now need to install ReactDOM. ReactDOM is a package that acts as glue between React and the DOM.
npm i react-dom
Installing Typings
Now lets install the type declaration files for React. Type declaration files, or .d.ts files, don't contain any implementation information and aren't compiled to JavaScript like .ts files are. Instead, the TypeScript compiler uses these files to point out incorrect usage of code such as providing the wrong amount of arguments to a function or accessing a property on an object that doesn't exist. Lets install the typings files for React and React DOM.
npm i @types/react @types/react-dom -D
Whenever we install a package from npm with the @types scope, we are installing typing information provided in .d.ts files.
Configuring TypeScript
Now lets turn our project into a TypeScript project. First, lets install TypeScript as a development dependency. TypeScript is simply an npm package called typescript.
npm i typescript -D
Now lets initialize this project as a TypeScript project by running the command npx tsc --init.
npx tsc --init
This creates a tsconfig.json file and fills it with some default values. We will be using this tsconfig.json configuration for type checking and .d.ts file generation. We will use Webpack and Babel for the actual transpilation process. To do this, we need to set a few keys.
"compilerOptions": {
"jsx": "react-jsx",
"declaration": true,
"emitDeclarationOnly": true,
"isolatedModules": true
}
- jsx - specifies what JSX code is generated.
- declaration - generates type files (.d.ts files) from TypeScript and JavaScript files.
- emitDeclarationOnly - only emit .d.ts files and not JavaScript files.
- isolatedModules - ensures that each TypeScript file can be transpiled without relying on other imports.
Lets also set the top level options include and exclude.
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
- include - specifies an array of filenames or patterns to include in the program.
- exclude - specifies an array of filenames or patterns that should be skipped when resolving include. We want to exclude any node_modules and dist folders.
Installing Webpack
Lets now install our module bundler Webpack as a development dependency. Webpack will transpile our TypeScript and React code to JavaScript that the browser understands.
npm i webpack -D
We also need to install the package webpack-cli (webpack command line interface).
npm i webpack-cli -D
This package provides a set of commands to increase speed/efficiency when setting up a custom webpack project. We also want to install the package webpack-dev-server.
npm i webpack-dev-server -D
This package gives us a web server to serve up our project and live reload it whenever changes are made.
Installing Babel Loader and Presets
To handle React and TypeScript code with Webpack, we need to install a loader called babel loader along with some other Babel dependencies.
npm i @babel/preset-env @babel/preset-react @babel/preset-typescript babel-loader -D
A loader is a function that Webpack passes code through to perform some sort of transformation. The Babel loader is a Webpack loader that transpiles JavaScript code. Babel presets are used to configure the Babel transipler.
- @babel/preset-env - Allows us to use the latest JavaScript.
- @babel/preset-react - Handles React code.
- @babel/preset-typescript - Handles TypeScript.
Handling HTML
Now lets create the HTML file to house our React app. We can do this using the html-webpack-plugin.
npm i html-webpack-plugin -D
A Webpack plugin interacts with the Webpack lifecycle. The html-webpack-plugin creates an HTML file to place our bundled JavaScript code into.
Configuring Webpack
Now lets configure Webpack with our webpack.config.cjs file. First, lets set the mode to development and set the entry point and output.
module.exports = {
mode: 'development',
target: 'web',
entry: './src/index.tsx',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
}
- mode - Sets the mode of the bundle. Setting to development sets process.env.NODE_ENV to development and enables meaningful names for modules/chunks for better debugging.
- target - Sets the target environment for the bundle. As this is a React app, it will be the web/browser.
- entry - Sets the entry point to start the bundling process.
- output - Sets the output path for the bundled JavaScript code.
Now lets add the html-webpack-plugin to Webpack's configuration using the top level plugins key.
const HtmlWebpackPlugin = require('html-webpack-plugin');
...
...
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
})
],
Now Webpack will add the bundled JavaScript code to the template HTML file in the bundle. Lets create the HTML file that the Webpack will add the bundle to.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
The div with the ID root is the element that will house our React application.
Handling TSX Code
Now lets configure Webpack to handle TypeScript and TSX code. This can be done by setting a rule.
module: {
rules: [
{
test: /\.(tsx|ts|jsx|js)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
['@babel/preset-react', {'runtime': 'automatic'}],
'@babel/preset-typescript'
]
}
}
},
]
}
Here we tell Webpack to pass all tsx, ts, jsx, and js files through the babel loader, with the provided presets, to convert it into code that the browser understands. Specifically, files ending in .tsx, .ts, .jsx, or .js excluding node_modules, will be transformed with the babel-loader. Finally, lets tell Webpack to resolve modules.
resolve: {
extensions: ['.tsx', '.ts', '.jsx', '.js']
}
Now we can leave off the extension for these files when importing them.
Handling CSS
To work with CSS, we need to use a CSS loader. A CSS loader transforms CSS into a string and loads it into a JavaScript file. An example of a CSS loader is css-loader from npm. Lets install it as a development dependency.
npm i css-loader -D
Now we need a way to get these styles to be displayed inside an HTML page. We can do this using another loader called style-loader. Install it from npm with npm i style-loader -D.
npm i style-loader -D
The style-loader takes the output from the css-loader and applies it to the DOM. Specifically, it takes the output from the css-loader and places it inside a <!-- webpack.config.cjs -->
rules: [
...
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
Note that the order in the array supplied to use is very important. We want our css-loader to be ran before our style-loader because first our CSS is transformed into a string and placed inside the JavaScript bundle file by the css-loader. This output is then placed into a style tag by the style-loader. Loaders proivded to use are executed from last to first, so here the css-loader will be ran on the CSS files and then the style-loader will be ran next.
Handling CSS Modules
Outputting the CSS to a style tag or a separate CSS file is useful but can lead to some issues with conflicts as it is global. To fix this, we can use CSS modules. CSS modules are scoped to the component that they are imported into. By default, the css-loader enables CSS modules for all files containing the following regex.
/\.module\.\w+$/i.test(filename)
/\.icss.\w+$/i.test(filename)
Therefore, if we want to use a CSS module we can create a file that ends in .module.css.
Webpack and Assets
Webpack allows us to bundle up assets such as images, icons, fonts, etc. Even better, Webpack has built in asset modules that allow us to handle assets. In other words, we no longer need to install and configure additional loaders.
rules: [
...
{
test: /\.(png|svg|jpg|jpeg|gif)$/,
type: 'asset/resource'
}
]
Setting the type key to asset/resource tells Webpack to emit a seprate file and export the URL. For example, if we import a PNG file into one of our TypeScript files, Webpack will add it to the bundle and export the URL to find it. Webpack also allows us to customize things like the name of the asset. By default Webpack emits assets by using the substitution [hash][ext][query] when type is set to asset/resource. We can change this by setting the assetModuleFilename key in the Webpack output object.
output: {
...
assetModuleFilename: 'images/[name][ext]'
},
Here, the output will create an images directory and fill it with the name of the asset followed by the extension.
Creating a globals.d.ts File
Now, as we will be importing assets into our TypeScript files, we need to inform the TypeScript compiler about these assets. We can do this by creating a globals.d.ts file.
declare module '*.css';
declare module '*.png';
declare module '*.jpg';
Using declare module tells TypeScript that we are defining types for files ending in .css, .png, and .jpg. We can then import these files into our TypeScript files and TypeScript understands what they are doing.
Writing Some CSS
Now lets write some CSS to style our application. First, we'll create a global styles file.
.center {
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
button {
height: 50px;
width: 200px;
font-size: xx-large;
margin: 20px;
}
img {
height: 500px;
width: 500px;
}
h1 {
color: red;
}
This file will contain styles applied throughout the application. Now lets create a more specific stylesheet. This one will be specific to styling the App component.
.cursive {
font-family: cursive;
}
h1 {
color: green;
}
Note how the file is called App.module.css. This is so it can be treated like a module when we import it into a component.
Writing the React Code
Now lets write our React code. First lets create our index.tsx file, the glue between the DOM and React.
import {createRoot} from 'react-dom/client';
import './styles/globals.css';
import App from './components/App';
const root = createRoot(document.getElementById('root') as HTMLElement);
root.render(<App />);
Note how we imported the global CSS styles directly. Now lets create the App component that we are rendering.
import {useState} from 'react';
import ocean from '../images/ocean.jpg';
import code from '../images/code.jpg';
import postgres from '../images/postgres.jpg';
import tree from '../images/tree.jpg';
import * as styles from '../styles/App.module.css';
export default function App() {
const [photo, setPhoto] = useState<string>(ocean);
const switchPhoto = () => {
if (photo === ocean) {
setPhoto(code);
} else if (photo === code) {
setPhoto(postgres);
} else if (photo === postgres) {
setPhoto(tree);
} else if (photo === tree) {
setPhoto(ocean);
}
};
return (
<div className='center'>
<h1 className={styles.cursive}>Photo Switcher</h1>
<div>
<img src={photo} />
</div>
<button onClick={switchPhoto}>Switch</button>
</div>
);
}
Note how we are importing the CSS module using a namespace import. This type of import contains all the exports from the module. Here, this is all the CSS classes, ids, etc.
Adding a Server to Serve up the Application
Lets now set up a server to serve up the application. For this we will use the Webpack dev server. Install it from npm as a development dependency.
npm i webpack-dev-server -D
Lets configure the Webpack dev server to serve up files on port 4444.
devServer: {
static: path.resolve(__dirname, 'dist'),
port: 4444
}
The static key is the path to serve static content from and port is the port to listen on. We want to serve the static content present in our dist folder.
Running the Application
To run this application lets create a simple npm start script.
"start": "webpack serve --open --config webpack.config.cjs",
Running npm start will now open a browser window and serve up the static files on port 4444.
npm start
> start
> webpack serve --open
<i> [webpack-dev-server] Project is running at:
<i> [webpack-dev-server] Loopback: http://localhost:4444/
<i> [webpack-dev-server] On Your Network (IPv4): http://192.168.0.23:4444/
<i> [webpack-dev-server] On Your Network (IPv6): http://[fe80::1]:4444/
<i> [webpack-dev-server] Content not from webpack is served from '/Users/wittcode/Desktop/create-a-typescript-app-with-webpack/dist' directory
<i> [webpack-dev-middleware] wait until bundle finished: /
asset bundle.js 621 KiB [emitted] (name: main)
asset index.html 233 bytes [emitted]
runtime modules 27.2 KiB 12 modules
Note that after running this command we don't see the dist folder present in the folder structure. This is because the webpack-dev-server holds it in memory.