Using CSS in React and TypeScript with Webpack 5
In the last post, we created a React app that used TypeScript and ESLint with Webpack. In this post, we will extend this Webpack configuration so that the app can use CSS.
Referencing an image in the app
Let’s start by trying to reference a CSS class in our App
component:
const App = () => <h1 className="app-heading">My React and TypeScript App!</h1>;
We’ll create a CSS file called app.css
to hold this CSS class definition:
.app-heading {
color: red;
}
Let’s start our app in dev mode by running npm start
in a terminal.
Of course, this doesn’t work - how does the App
component know where the app-heading
is defined? We can use an import statement as follows to do this:
import "./app.css"
Webpack gives a “Module parse failed: Unexpected token” error because it doesn’t understand imports for CSS files yet:
Configuring CSS for development
We need to tell Webpack how to handle CSS files. We need to add a couple of loaders as follows in the dev Webpack config (webpack.dev.config
in our example):
const config: Configuration = {
...
module: {
rules: [
...,
{ test: /\.css$/i, use: ["style-loader", "css-loader"], }, ],
},
...
};
So, when Webpack comes across a .css
file, it handles it with css-loader
and style-loader
(loaders in the use
array are executed in the reverse order 🤔).
css-loader
reads the referenced CSS file in the import statement (app.css
in our example). style-loader
then put this CSS content into a style
element in the bundled html file. While the style
element isn’t ideal in production, it is nice in development because the Webpack dev server can make changes to the style
element quickly.
For our new Webpack configuration to work, we need to install both the css-loader
and style-loader
packages as follows:
npm install --save-dev css-loader style-loader
We will need to stop and restart the app to see this in action:
Configuring CSS for production
In production, we want the CSS to be in an external CSS file so that our app can take advantage of browser caching. We can do this by using mini-css-extract-plugin
instead of style-loader
.
Let’s start by installing mini-css-extract-plugin
with its TypeScript types:
npm install --save-dev mini-css-extract-plugin @types/mini-css-extract-plugin
We can now add this loader to our production webpack config (webpack.prod.config
in our example):
...
import MiniCssExtractPlugin from "mini-css-extract-plugin";
const config: Configuration = {
...,
module: {
rules: [
...,
{ test: /\.css$/i, use: [MiniCssExtractPlugin.loader, "css-loader"], }, ],
},
...,
plugins: [
...,
new MiniCssExtractPlugin({ filename: "[name].[contenthash].css", }), ],
...
};
mini-css-extract-plugin
needs to do a little more work than loading content into the existing bundles - it needs to create additional files (.css
files). This is why it is a plugin.
We have used the [name]
token in the CSS file name to allow Webpack to name the files if our app is code split. We have used the [contenthash]
token to change the bundle file name when its content changes, which will bust the browser cache.
Let’s do a build:
npm run build
We’ll see a CSS file generated in the build
folder:
If we look in the HTML file, we’ll see the external CSS file referenced:
Nice. 🙂
Styles from multiple components
Let’s restructure our App
component so that it references two child components, which in tern reference different CSS files:
import "./heading.css";
import "./content.css";
const App = () => (
<>
<Heading />
<Content />
</>
);
const Heading = () => <h1 className="heading">My React and TypeScript App</h1>;
const Content = () => <div className="content">With CSS!</div>;
Our CSS files are as follows:
/* heading.css */
.heading {
color: red;
}
/* content.css */
.content {
color: green;
}
If we run the app in dev mode (npm start
), we will see the components are styled as we expect by two style elements on the HTML page:
If we do a build (npm run build
), we will see that the CSS is bundled into a single CSS file:
Excellent! 🙂
Using CSS modules
The problem with this approach is that the developers need to carefully name the CSS classes so that they don’t clash. For example, if we have called both of our CSS classes “text”, what would the heading and content color be?
The second CSS class takes precedence over the first, which results in both pieces of text being green.
CSS modules solve this problem by allowing us to scope CSS to a particular component. Let’s adjust our components to reference CSS modules:
import heading from "./heading.module.css";
import content from "./content.module.css";
...
const Heading = () => (
<h1 className={heading.heading}>My React and TypeScript App</h1>
);
const Content = () => <div className={content.content}>With CSS!</div>;
The import statement is slightly different. We are referencing a file with a .module.css
suffix and importing a variable from the file which will contain all the CSS class names. Our components reference the class name variables - we will see why this is the case when we see the output.
We will need to rename content.css
to content.module.css
and heading.css
to heading.module.css
.
TypeScript raises a “Cannot find module ‘.module.css’ or its corresponding type declaration” error because it doesn’t know how to handle CSS modules:
To resolve this error, we can add the following content into a file called custom.d.ts
in the src
folder:
declare module "*.module.css";
This resolves the TypeScript error.
If we restart the app in dev mode, we see the styles are applied as we expected:
We see that the CSS class names have been given a random-looking name. This ensures that styles in different components don’t clash.
If we remember back to how we referenced the class name in a component, it was a little strange:
className={fileName.cssClassName}
This allows the Webpack process to give the class name a unique name.
If we produce a production build (npm run build
), we get unique class names in the production CSS file too:
Nice! 🙂
That’s it! Our React and TypeScript project is now set up to use CSS.
This code is available in GitHub at https://github.com/carlrip/react-typescript-css-webpack.
Did you find this post useful?
Let me know by sharing it on Twitter.If you to learn more about using TypeScript with React, you may find my course useful: