with webpack, Babel, TypeScript, Sass, CSS Module, Tailwind, React Refresh(HMR)
In this tutorial, we're going to set up a React application without using create-react-app.
We're going to learn how to set up from scratch with TypeScript, Sass, Tailwind CSS, CSS module, absolute paths, and more.
TL;DR My GitHub Repository
Basic concepts you need to know beforehand
1. TSC does type checking, Babel does transpiling
- TSC(TypeScript Compiler) that is included in the TypeScript package will only be used as a type checker of your source codes, and you can achieve it by setting:
{
"compilerOptions": {
...
"noEmit": true, /* Disable emitting files from a compilation. */
...
}
}
-
@babel/preset-typescript
will be used to compile(transpile) TypeScript to JavaScript- why I use
@babel/preset-typescript
over ts-loader and awesome-typescript-loader to compile TypeScript?- it's blazingly faster than the others and has more perks(read #4)
-
ts-loader
doesn't natively support HMR(Hot Module Replacement) -
awesome-typescript-loader
's latest release is on Jun 22, 2018 - TypeScript With Babel: A Beautiful Marriage - Matt Turnbull's Post
- why I use
2. We're gonna try the Fast Refresh Feature
The Fast Refresh is a React Native feature that allows you to get near-instant feedback for changes in your React components However, my idol, Dan Abramov talked about applying it across the board in 2019 - as a replacement for purely userland solutions (like react-hot-loader).
If you navigate to the React Hot Loader
page on npm, you'll see
as mentioned above, we're using the webpack; therefore, we're going to use @pmmmwh/react-refresh-webpack-plugin to apply the Fast Refresh feature to our app. I mean, it's for development, so what's the harm of trying it right?
3. What's up with all the CSS libraries such as CSS Module, Sass, and Tailwind CSS
I'm not saying you should use all of these libraries, but it's just to show you how to set up these for your future projects. In my case, I use Sass and CSS Module combination when I work on page components, and the Tailwind CSS when I need to create prototypes fast and when I make reusable small components.
Okay, enough with the prerequisites.
Let's begin.
Step 1. Create a folder for your project
$ mkdir my-app && cd my-app # create and go to my-app folder
Step 2. Generate package.json file
$ yarn init
then fill in the questions shown below following the prompt
if you prefer to generate one based on your own defaults, use:
$ yarn init --yes # or '-y' in short
if you don't have yarn
installed please follow this link.
You can definitely change the settings later on, so don't worry.
Step 3. Add React and React-related packages
$ yarn add react react-dom react-router-dom
$ yarn add --dev @types/react @types/react-dom @types/react-router-dom # for TypeScript
-
react(v18.2.0) : The
react
package contains only the functionality necessary to define React components. It is typically used together with a React renderer likereact-dom
for the web, orreact-native
for the native environments. -
react-dom(v18.2.0): This package serves as the entry point to the DOM and server renderers for React. It is intended to be paired with the generic React package, which is shipped as
react
to npm. - react-router-dom(v6.4.4): contains bindings for using React Router in web applications.
-
@types/react
,@types/react-dom
,@types/react-router-dom
from TypeScript
Step 4. Add webpack and webpack cli packages
with the Webpack, you can combine all of your React codes into one or more bundles, that are static assets including not only JavaScript or TypeScript files, but also Sass or Image files(png, jpg)
$ yarn add --dev webpack webpack-cli webpack-dev-server
$ yarn add --dev @types/webpack @types/webpack-dev-server # for TypeScript
- webpack(v5.75.0)
- webpack-cli(v5.0.1): enables you to use the command-line interface of the Webpack
- webpack-dev-server(v4.11.1)
-
@types/webpack
,@types/webpack-dev-server
: for TypeScript
Step 5. Add Babel core and preset for React packages
Babel is a compiler(or transpiler) and a toolchain that is mainly used to convert ECMAScript 2015+ code into a backward-compatible version of JavaScript in current and older browsers or environments. The preset-react
package will convert JSX syntax to JavaScript codes that browsers can understand.
$ yarn add --dev @babel/core @babel/preset-env @babel/preset-react
$ yarn add --dev @babel/preset-typescript # for TypeScript
$ yarn add --dev @babel/register # to use webpack.config.ts (TypeScript)
- @babel/core(v7.20.5): core files of Babel compiler
Presets for Babel
- @babel/preset-env(v7.20.2): allow you to use the latest JavaScript without needing to micromanage which syntax transforms (and optionally, browser polyfills) are needed by your target environment(s)
- @babel/preset-react(v7.18.6): process and transform jsx syntax and display name
-
@babel/preset-typescript(v7.18.6)
- since ts-loader for the webpack doesn't natively support HMR, I prefer Babel's preset to handle TypeScript, also because basically webpack works as a module bundler and Babel works as a compiler(transpiler).
-
@babel/register(v.7.18.9): The require hook will bind itself to node's
require
and automatically compile files on the fly. You need it when you want to use the webpack config file as TypeScript. If you usewebpack.config.ts
, and thewebpack cli
will complainUnable load /{path}/webpack.config.ts
and
Unable to use specified module loader for ".ts"
if you don't have
@babel/register
. No need extra configurations.
webpack and Babel packages are added using
--dev | -D
flag not to include those in the final code bundle
Step 6. Create babel.config.json file
{
// there's room for discussions, but I won't exclude node_modules here
// https://stackoverflow.com/questions/54156617/why-would-we-exclude-node-modules-when-using-babel-loader
// "exclude": "node_modules/**",
"presets": [
"@babel/preset-env",
[
"@babel/preset-react",
{
"runtime": "automatic" // the default option, "classic" does not automatic import anything.
// https://stackoverflow.com/questions/32070303/uncaught-referenceerror-react-is-not-defined
}
],
"@babel/preset-typescript" // transpile typescript
]
}
Step 7. Add loaders for webpack
Disclaimer: these loaders are third-party packages maintained by community members, it potentially does not have the same support, security policy or license as webpack, and it is not maintained by webpack.
- webpack documentation
There are instructions below about implementations of the loaders listed below; however, you can go to the links and follow the instructions if you encounter any issues along the way.
$ yarn add --dev babel-loader # to connect Babel and webpack
$ yarn add --dev html-webpack-plugin html-loader # for HTML
$ yarn add --dev style-loader css-loader sass-loader postcss postcss-loader postcss-preset-env mini-css-extract-plugin # for CSS
$ yarn add --dev react-refresh @pmmmwh/react-refresh-webpack-plugin # for HMR
# connect babel and webpack
- babel-loader(v9.1.0): allows transpiling JavaScript files using Babel and webpack.
# HTML-related
-
html-webpack-plugin(v5.5.0): generate an HTML5 file for you that includes all your webpack bundles in the body using
script
tags. You can use your own template as well. - html-loader(v4.2.0): exports HTML as string. HTML is minimized when the compiler demands.
# style-related
-
style-loader(v3.3.1): inject CSS into the DOM. It will be only used when it's on development.
MiniCssExtractPlugin
will be used for production.- Simply, the
style-loader
directly injects CSS inside style tags in the DOM,MiniCssExtractPlugin
bundles your CSS and creates separate CSS files. - One of the reasons I decided to use as above is that when using the
style-loader
, it's imperative receiving unnecessary inline styles whereas you can load css files on demand when usingMiniCssExtractPlugin
. It doesn't make a huge difference if your application is relatively small thou.
- Simply, the
-
css-loader(v6.7.2): interpret
@import
andurl()
likeimport/require()
and will resolve them.You need the two loaders above even if you don't wanna use Sass or Tailwind
-
postcss-loader(v7.0.2): allow using postcss and to use Tailwind CSS.
- w/ postcss(v8.4.19): a tool for transforming styles with JS plugins. These plugins can lint your CSS, support variables and mixins, transpile future CSS syntax, inline images, and more.
- w/ postcss-preset-env(v7.8.3): convert modern CSS into something most browsers can understand, determining the polyfills you need based on your targeted browsers or runtime environments. It takes the support data that comes from MDN and Can I Use and determine from a browserlist whether those transformations are needed. It also packs Autoprefixer within and shares the list with it, so prefixes are only applied when you're going to need them given your browser support list.
Since we're going to use Sass and Tailwind CSS in addition to the native CSS, as recommended on the Tailwind's official documentation, we're going to use PostCSS. *You don't have to install
autoprefixer
because it is already included in thepostcss-preset-env
-
sass-loader(v13.2.0): load a Sass/SCSS file and compiles it to CSS.
- w/ sass(v1.56.2): Sass is a stylesheet language that’s compiled to CSS. It allows you to use variables, nested rules, mixins, functions, and more, all with a fully CSS-compatible syntax.
MiniCssExtractPlugin(v2.7.2): extract CSS into separate files. It creates a CSS file per JS file which contains CSS. It supports On-Demand-Loading of CSS and SourceMaps. We're going to use it in production.
# HMR (Hot Module Replacement) related (development only)
What is 'Hot Module Replacement'
instead of using classic HotModuleReplacementPlugin
, we're going to implement "Fast Refresh" with react-refresh
and @pmmmwh/react-refresh-webpack-plugin
.
TMI: Next.js natively supports the Fast Refresh
react-refresh
implements the wiring necessary to integrate Fast Refresh into bundlers. Fast Refresh is a feature that lets you edit React components in a running application without losing their state. It is similar to an old feature known as "hot reloading", but Fast Refresh is more reliable and officially supported by React.
Disclaimer: @pmmmwh/react-refresh-webpack-plugin
is not 100% stable
- @pmmmwh/react-refresh-webpack-plugin(v0.5.10)
- react-refresh(v0.14.0)(rough guide from #gaearon)
Step 8. Add extra
$ yarn add --dev typescript-plugin-css-modules # to use CSS module in TypeScript
- typescript-plugin-css-modules(v4.1.1): A TypeScript language service plugin for CSS Modules.
- *if you don't use CSS Modules, obviously you don't have to install it.
After everything is installed, your package.json
should look something like this:
{
"name": "my-app",
"version": "0.0.1",
"description": "Brandon's complete guide for manual react app setup with TypeScript ",
"main": "./index.js",
"author": "brandonwie",
"license": "MIT",
"private": false,
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.4.5"
},
"devDependencies": {
"@babel/core": "^7.20.5",
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.18.6",
"@babel/register": "^7.18.9",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9",
"@types/react-router-dom": "^5.3.3",
"@types/webpack": "^5.28.0",
"@types/webpack-dev-server": "^4.7.2",
"babel-loader": "^9.1.0",
"css-loader": "^6.7.2",
"html-loader": "^4.2.0",
"html-webpack-plugin": "^5.5.0",
"mini-css-extract-plugin": "^2.7.2",
"postcss": "^8.4.19",
"postcss-loader": "^7.0.2",
"postcss-preset-env": "^7.8.3",
"react-refresh": "^0.14.0",
"sass": "^1.56.2",
"sass-loader": "^13.2.0",
"style-loader": "^3.3.1",
"tailwindcss": "^3.2.4",
"typescript": "^4.9.3",
"typescript-plugin-css-modules": "^4.1.1",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.11.1"
}
}
Step 9. Create tsconfig.json
$ touch tsconfig.json
# or you can let tsc to handle it
$ tsc --init
it should look something like this:
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Language and Environment */
//NOTE @brandonwie target latest version of ECMAScript
"target": "ESNext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"lib": [
"DOM",
"DOM.Iterable",
"ESNext"
] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
"jsx": "react-jsx" /* Specify what JSX code is generated. */,
/* Modules */
"module": "ESNext" /* Specify what module code is generated. */,
//NOTE @brandonwie Search under node_modules for no-relative imports
"moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
"baseUrl": "./src" /* Specify the base directory to resolve non-relative module names. */,
"paths": {
"@*": ["*"]
} /* Specify a set of entries that re-map imports to additional lookup locations. */,
"types": [
"node"
] /* Specify type package names to be included without being referenced in a source file. */,
"resolveJsonModule": true /* Enable importing .json files. */,
/* JavaScript Support */
//NOTE @brandonwie Process & infer types from .js files
"allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */,
"checkJs": true /* Enable error reporting in type-checked JavaScript files. */,
/* Emit */
//NOTE @brandonwie Don't emit; allow Babel to tranform files
"noEmit": true /* Disable emitting files from a compilation. */,
//NOTE @brandonwie Disallow features that require cross-file information for emit
"isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */,
"allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */,
//NOTE @brandonwie Import non-ES modules as default imports
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
//NOTE @brandonwie Enable strictest settings like strictNullChecks & noImplicitAny
"strict": true /* Enable all strict type-checking options. */,
"noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */,
"strictNullChecks": true /* When type checking, take into account 'null' and 'undefined'. */,
"noFallthroughCasesInSwitch": true /* Enable error reporting for fallthrough cases in switch statements. */,
"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"include": ["src", "declaration.d.ts"] /* Include modules in the program. */,
"exclude": ["node_modules", "build"] /* Exclude modules from the program. */,
"plugins": [{ "name": "typescript-plugin-css-modules" }] // to use css modules
}
Some of the settings above are personal; however, settings that aren't boolean are necessary.
Step 10. Create webpack.config.ts
$ touch webpack.config.ts
it should look something like this:
import path from 'path';
import webpack from 'webpack';
import HTMLWebpackPlugin from 'html-webpack-plugin';
//create css file per js file: https://webpack.kr/plugins/mini-css-extract-plugin/
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin';
const isDevelopment = process.env.NODE_ENV !== 'production';
// define plugins
const plugins: webpack.WebpackPluginInstance[] = [
new HTMLWebpackPlugin({
template: './public/index.html', // you have to have the template file
}),
];
isDevelopment
? plugins.push(new ReactRefreshWebpackPlugin())
: plugins.push(new MiniCssExtractPlugin());
const config: webpack.Configuration = {
mode: isDevelopment ? 'development' : 'production',
devServer: {
hot: true,
port: 3000,
},
entry: './src/index.tsx', // codes will be inside src folder
output: {
path: path.resolve(__dirname, 'build'),
filename: 'index.js',
// more configurations: https://webpack.js.org/configuration/
},
plugins,
resolve: {
modules: [path.resolve(__dirname, './src'), 'node_modules'],
// automatically resolve certain extensions (Ex. import './file' will automatically look for file.js)
extensions: ['.ts', '.tsx', '.js', '.jsx', '.scss', '.css'],
alias: {
// absolute path importing files
'@pages': path.resolve(__dirname, './src/pages'),
},
},
module: {
rules: [
{
test: /\.html$/,
use: ['html-loader'],
},
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
use: [
{
loader: require.resolve('babel-loader'),
options: {
plugins: [
isDevelopment && require.resolve('react-refresh/babel'),
].filter(Boolean),
},
},
],
},
{
test: /\.(sa|sc|c)ss$/i, // .sass or .scss
use: [
// Creates `style` nodes from JS strings
'style-loader',
// Translates CSS into CommonJS
'css-loader',
// for Tailwind CSS
'postcss-loader',
// Compiles Sass to CSS
'sass-loader',
],
},
],
},
};
export default config;
The code above is based on many other references, so I recommend you to play along with the settings when you finish the setup. Also, you can use .js
file instead of .ts
file and don't use @babel/register
since I just wanted to try it with TypeScript.
Step 11. Create postcss.config.js
$ touch postcss.config.js
it should look something like this:
module.exports = {
// didn't add autoprefixer because it is already included in postcss-preset-env
plugins: [require('tailwindcss'), require('postcss-preset-env')],
};
Step 12. Create tailwind.config.js
$ touch tailwind.config.js
it should look something like this:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{html,js,jsx,ts,tsx}'],
theme: {
extend: {},
},
plugins: [],
};
Step 13. Create public folder and index.html file to use it as a template
From the setting above, we told the webpack to find ./public/index.html
and use it as a template file
const plugins: webpack.WebpackPluginInstance[] = [
new HTMLWebpackPlugin({
template: './public/index.html', // you have to have the template file
}),
];
$ mkdir public && touch public/index.html
<!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>Brandon's App'</title>
</head>
<body>
<div id="root"></div> <!-- for react -->
</body>
</html>
If you use VSCode, simply you can type !
and enter
to create the HTML codes.
Step 14. Create src folder, index.tsx, and App.tsx
$ mkdir src && touch src/index.tsx src/App.tsx
Your entry is set to index.tsx
in the src
folder in webpack.config.ts
file
const config: webpack.Configuration = {
mode: isDevelopment ? 'development' : 'production',
devServer: {
hot: true,
port: 3000,
},
entry: './src/index.tsx', // codes will be inside src folder
...
}
Your index.tsx
file should look something like this:
import { createRoot } from 'react-dom/client';
import App from './App';
const container = document.getElementById('root');
const root = createRoot(container!);
root.render(<App />);
If you've been using React version 17 or below, you may find the rendering method is somewhat different. Please follow the link, How to Upgrade to React 18.
Your App.tsx
file may look something like this:
import styles from './App.module';
import SamplePage from '@pages/Sample';
import './main.css';
const App: React.FC = () => {
return (
<div>
<div className={styles.title}>CSS module works!</div>
<div className={styles.subtitle}>CSS module + Tailwind works!</div>
<div
className={
'border-[10px] border-solid border-red-800 rounded-full w-[200px] h-[200px] flex items-center justify-center text-center'
}
>
Tailwind works!
</div>
<SamplePage />
</div>
);
};
export default App;
I hope you get the ideas here. What we've been setting are now available in our codes.
🙏 Please visit my GitHub repository for more settings and codes.
Step 15. (optional) add scripts to package.json
"scripts": {
"start": "webpack serve --hot --mode development",
"build": "webpack --mode production"
}
Now you can run the app with yarn start
This is what you'll see if you run the app.
That's it for this post guys. Thank you so much for reading.
Next post will be about prettier and eslint settings using eslint-config-airbnb. See you next time! 👋🏼
References
- Creating your React project from scratch without create-react-app: The Complete Guide(dev.to)
- Speeding up your development with Webpack 5 HMR and React Fast Refresh(dev.to)
- Setup a React app with Webpack and Babel(dev.to)
- Setup a React App using Webpack, Babel and TypeScript(dev.to)
- Webpack style-loader vs css-loader(stackoverflow)
- Webpack style-loader vs css-loader(stackoverflow)
- Webpack 러닝 가이드(learning guide) - KR(gitbook.io)
Top comments (6)
Hello, just wanted to congratulate you on the amazing post. It really solved many doubts that I had. One thing I had trouble with was
tsconfig.json
file. At first, the IDE was showing an error related to theesModuleInterop
andallowSyntheticDefaultImports
keys (TS1259: Module '"path"' can only be default-imported using the 'esModuleInterop' flag), but it stopped when I included the filewebpack.config.ts
in theinclude
key oftsconfig.json
. My question is, is it wrong to addwebpack.config.ts
in theinclude
key?Thanks for your comment. :)
Theoratically, I don't think it's wrong to add
webpack.config.ts
inside theinclude
array.Check out this link on Stackoverflow about using TypeScript as Webpack config.
Hope you find it helpful.
Happy Coding!
Thank you! This is the best guid I've found so far with Webpack, Typescript and SASS together
Hi @sergsar, thank you so much for your comment. I've been struggling for 3 days for this post :)
There will be some small updates of this post because I found some small issues while actually using this boiler template for my application.
Any suggestions with setup eslint and prettfier with @babel/preset-typescript would be great also
I'll work on it as soon as I get some of my tasks done. Happy Coding!