Rivet Icons with Webpack and React
This repo shows how to make Rivet Icons part of a Webpack and React project.
Goals:
- Build a custom icon set, with select Rivet icons and custom icons.
- Import icon dependencies with Webpack.
- Use the
<rvt-icon>
custom element inside of React. - Use inline icons (without the
<rvt-icon>
custom element or SVG symbols).
Note: The compiled project is in the ./docs
folder, which is used by GitHub Pages to publish it to the demo site.
Contents
1. Build a custom icon set
Install rivet-icons
.
npm install --save rivet-icons
Create a Node script to configure the icon set as desired. In this case, the only Rivet icons wanted are the heart icons. Using a glob string (heart*
), all these icons can be included, without individually declaring each one. Custom icons in the src/assets
folder are included as well. Output generated files to a ./build
folder. This async build function will automatically execute, whenever the script is called.
// ./scripts/icons.js
const { buildIcons } = require('rivet-icons')
async function build () {
await buildIcons({
icons: ['heart*'],
include: ['src/assets/*'],
out: 'build'
})
}
build()
Add the build folder to .gitignore
. This folder is meant for temporary files, which shouldn't be committed to the repo.
# .gitignore
build
Install npm-run-all
and rimraf
. npm-run-all
sequences scripts. rimraf
recursively deletes folders.
npm install --save-dev npm-run-all rimraf
Add npm scripts to package.json
to build the icons. The build
script triggers the ./build
folder to be deleted, then generates the custom icon set from ./scripts/icons.js
.
{
"scripts": {
"build": "run-s rmdir-build build-icons",
"build-icons": "node scripts/icons.js",
"rmdir-build": "rimraf build"
}
}
Test the script.
npm run build
2. Set up Webpack
Install Webpack.
npm install --save-dev webpack webpack-cli webpack-dev-server
Install Babel and supplemental packages. These allow Babel to transpile the code for older browsers (@babel/preset-env
), interpret React JSX (@babel/preset-react
), and integrate with Webpack (babel-loader
).
npm install --save-dev @babel/core @babel/preset-env @babel/preset-react babel-loader
Configure Babel with a .babelrc
file.
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
]
}
Configure Webpack with a webpack.config.js
file. The entry point for the application will be ./src/index.jsx
, but it will be compiled to ./docs/app.js
. Any .js
or .jsx
files in the ./src
folder will be passed to Babel.
Note: An out folder other than ./docs
is likely more appropriate for a real project. The folder name is a limitation of using GitHub Pages to host the generated files. Additionally, the out folder should likely be added to .gitignore
, to keep derived files out of the repo. It was not done in the case of this project, to make it easy to see the output, without installing the project.
// webpack.config.js
const path = require('path')
const srcPath = path.resolve('./src')
const outPath = path.resolve('./docs')
module.exports = {
entry: {
app: path.resolve(srcPath, 'index.jsx')
},
output: {
filename: `[name].js`,
path: outPath
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
use: ['babel-loader'],
include: srcPath
}
]
}
}
Extend the npm build
script to delete the out folder (./docs
) before every run, and run Webpack after building the icon set.
{
"scripts": {
"build": "run-s rmdir-build rmdir-docs build-icons build-webpack",
"build-icons": "node scripts/icons.js",
"build-webpack": "webpack --mode=production",
"rmdir-build": "rimraf build",
"rmdir-docs": "rimraf docs"
}
}
Add a start
script to run a development environment with Webpack Dev Server. This builds the icon set before starting the server. It doesn't delete the out folder, since it doesn't use it (unlike the build
script).
{
"scripts": {
"start": "run-s rmdir-build build-icons start-webpack",
"start-webpack": "webpack serve --open --hot --mode=development"
}
}
3. Set up React
By default, Webpack bundles all imported dependencies into a single out file. This can lead to performance problems when importing large dependencies or using dependencies that would be better shared across multiple entry points or pages. Additionally, many libraries, such as React, provide production-ready bundles that do not need further processing by the consuming application.
Extend webpack.config.js
with an externals
configuration. The key is the name of the import. The value is the name of the window
global variable of that library.
// webpack.config.js
{
externals: {
react: 'React',
'react-dom': 'ReactDOM'
}
}
// This:
import React from 'react'
import ReactDOM from 'react-dom'
// Is interpreted as this:
const React = window.React
const ReactDOM = window.ReactDOM
At this point, React could be referenced by an external provider, like UNPKG. These links go to the latest production bundles that can be used in any environment (the browser or Node).
<head>
<script defer src="https://unpkg.com/react/umd/react.production.min.js"></script>
<script defer src="https://unpkg.com/react-dom/umd/react-dom.production.min.js"></script>
</head>
Note: These scripts are placed in <head>
and use the defer
attribute. This means the scripts are requested early, they don't block rendering the rest of the page, and they get processed in declaration order after the page is loaded. It behaves similar to placing the scripts at the end of <body>
, but since the network request happens earlier, it finishes faster.
While it is fine to use external services for some cases, for production, it is likely better to directly control and provide these resources. The application should declare these dependencies in the code, so then Webpack can essentially copy-and-paste the files into the out folder.
Install React.
npm install --save react react-dom
In the entry file, import the production-ready React dependencies and add the resource query ?asset
to the end. This should be done only once in the application, and ideally as early as possible.
// ./src/index.jsx
import 'react/umd/react.production.min.js?asset'
import 'react-dom/umd/react-dom.production.min.js?asset'
Resource queries provide a hook for Webpack to apply specific rules to the given import. The ?asset
query is a custom Module Rule that will copy the imported file to an assets
folder in the out folder.
// webpack.config.js
{
module: {
rules: [
{
resourceQuery: /asset/,
type: 'asset/resource',
generator: {
filename: 'assets/[base]'
}
}
]
}
}
// ./src/index.jsx
import 'react/umd/react.production.min.js?asset'
import 'react-dom/umd/react-dom.production.min.js?asset'
// Webpack outputs:
// ./docs/assets/react.production.min.js
// ./docs/assets/react-dom.production.min.js
This same resource query can be used to copy other assets, such as Rivet's rivet-core
(make sure to install it), assets from rivet-icon
, and custom build files.
// ./src/index.jsx
import 'rivet-core/css/rivet.min.css?asset'
import 'rivet-icons/dist/rivet-icon-element.js?asset'
import 'rivet-icons/dist/rivet-icons.css?asset'
import '../build/rivet-icons.js?asset'
// Webpack outputs:
// ./docs/assets/rivet.min.css
// ./docs/assets/rivet-icon-element.js
// ./docs/assets/rivet-icons.css
// ./docs/assets/rivet-icons.js
4. Set up the page
Now that Webpack is building and copying assets, the browser needs an entry point to the application. Create ./src/index.html
. Reference the dependencies relative to the out folder.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My App</title>
<link rel="stylesheet" href="./assets/rivet.min.css">
<link rel="stylesheet" href="./assets/rivet-icons.css">
<script defer src="./assets/react.production.min.js"></script>
<script defer src="./assets/react-dom.production.min.js"></script>
<script defer src="./assets/rivet-icons.js"></script>
<script type="module" src="./assets/rivet-icon-element.js"></script>
<script defer src="./app.js"></script>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
A new resource query can be used to copy files to the root of the out folder, rather than to the assets
subfolder. This could be used for the index page and external styles.
// webpack.config.js
{
module: {
rules: [
{
resourceQuery: /root/,
type: 'asset/resource',
generator: {
filename: '[base]'
}
}
]
}
}
// ./src/index.jsx
import './index.html?root'
import './styles.css?root'
// Webpack outputs:
// ./docs/index.html
// ./docs/styles.css
Because all dependencies were imported with Webpack through the entry file (./src/index.jsx
), Webpack Dev Server is aware of them and will serve them. Use the devServer.contentBase
configuration if there are files needed to be included by Webpack Dev Server but excluded from the out folder.
Run the development environment. Confirm "Hello World" displays and that all resources are served.
npm run start
Finally, set up a mount point for React and render to it. Confirm "Hello World, from React" displays.
<body>
<div id="app"></div>
</body>
// ./src/index.jsx
import React from 'react'
import { render } from 'react-dom'
function App () {
return (
<h1>Hello World, from React</h1>
)
}
render(
<App />,
document.getElementById('app')
)