A guide to building a React component with webpack 4, publishing to npm, and deploying the demo to GitHub Pages

You have developed a React component that you want to share with the community, but you are not exactly sure how to get it out there. This guide will walk you through the process.

Let’s assume you originally developed the component as part of another project, which you set up with create-react-app. Publishing your component for community consumption involves a couple of special workflows which require a custom project setup.

In this guide, we will be building this project setup completely from scratch. In the process, you may come to better appreciate what react-scripts has been doing for you behind the scenes. I know I did.

The project we are going to build streamlines and automates the following workflows:

  1. Build and locally demo your component using Webpack 4, with automatic refresh when source files change.
  2. Publish a transpiled, ready-to-use version of your component to npm.
  3. Publish an online demo to GitHub Pages.

We will take each workflow in turn. Let’s start from the beginning.

Build and locally demo your component

Create the project folder and initialize the npm package. Be sure to choose a name for your component that is not already in use on npm; here we are using my-component as a placeholder.

mkdir my-component
cd my-component
npm init

Accept the default answers when prompted by npm init.

To locally demo our component, we obviously need React, so let’s install it as a dev dependency. Later we will see how to specify that our component expects React to exist in projects where it is installed from npm.

npm i react react-dom -D

Our project build will be handled by Webpack, which will use Babel for transpiling and Webpack Dev Server for local serving. Lets install these dev dependencies as well.

npm i webpack webpack-cli webpack-dev-server html-webpack-plugin style-loader css-loader babel-core babel-loader@7.1.4 babel-preset-env babel-preset-react -D

By this point, npm will have built up a fairly chunky package.json file in the project root folder. Let’s add a start script to this file which we will use to spin up our dev environment. We will go into more detail on this in a moment.

Add the following modifications (in bold) to package.json:

{
"name": "my-component",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "webpack-dev-server --mode development"
},
"author": "",,
"license": "ISC",
"devDependencies": {
"babel-core": "^6.26.0",
"babel-loader": "^7.1.4",
"babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.24.1",
"css-loader": "^0.28.11",
"html-webpack-plugin": "^3.2.0",
"react": "^16.3.2",
"react-dom": "^16.3.2",
"style-loader": "^0.20.3",
"webpack-dev-server": "^3.1.3"
}
}

You can now add your component to the project, and create a demo for testing and showing it off. But first, let’s set up the following folder structure within the project root, which conveniently isolates our component source tree from our demo source tree.

src\   <- component source & styles
examples\src\ <- demo page

I will create a trivial component which will serve as a reference point for the rest of the article.

/*** src/index.js   ***/
import React from 'react';
import './styles.css';
const MyComponent = () => (
<h1>Hello from My Component</h1>
);
export default MyComponent;

/*** src/styles.css ***/
h1 {
color: red;
}

The community is going to love it!

Now let’s add a demo.

<!-- examples/src/index.html -->
<html>
<head>
<title>My Component Demo</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
</body>
</html>

/***  examples/src/index.js ***/
import React from 'react';
import { render} from 'react-dom';
import MyComponent from '../../src';
const App = () => (
<MyComponent />
);
render(<App />, document.getElementById("root"));

Note that the demo imports MyComponent from ../../src.

Let’s set up Webpack. Add webpack.config.js to the project root:

/*** webpack.config.js ***/
const path = require('path');
const HtmlWebpackPlugin = require("html-webpack-plugin");
const htmlWebpackPlugin = new HtmlWebpackPlugin({
template: path.join(__dirname, "examples/src/index.html"),
filename: "./index.html"
});
module.exports = {
entry: path.join(__dirname, "examples/src/index.js"),
module: {
rules: [
{
test: /\.(js|jsx)$/,
use: "babel-loader",
exclude: /node_modules/
},
{
test: /\.css$/,
use: ["style-loader", "css-loader"]
}
]
},
plugins: [htmlWebpackPlugin],
resolve: {
extensions: [".js", ".jsx"]
},
devServer: {
port: 3001
}
};

This configures Webpack to do the following:

  • Resolve source dependencies using examples/src/index.js as a starting point
  • Use Babel to transpile .js and .jsx files via babel-loader
  • Resolve CSS dependencies and inject inline styles via css-loader and style-loader
  • Automatically inject a script reference to the bundle output in examples/src/index.html via html-webpack-plugin
  • Serve the demo on port 3001

Finally, we need to tell Babel which transpilations we want. We definitely want to transpile JSX syntax into React core API calls. Let’s say we also want to transpile javascript down to ES5 to reach more browsers. This is a common configuration which is easily applied via a couple of presets. Add a .babelrc file to the project root like so:

{
"presets": ["env", "react"]
}

Let’s run the demo:

npm start

Point your browser to localhost:3001 and you should see the demo page for your component. Make some changes and hit save, and the page will automatically refresh. Our dev environment is working in watch mode!

Let’s proceed with the setup needed to support the second workflow.

Publish a transpiled, ready-to-use version of your component to npm

Publishing to npm is an easy, automated process, but there are a couple of setup steps to take first.

We want to publish a Babel-transpiled version of the minimum files needed to install and use the component. Pre-transpilation prepares the component for use even in target projects that are not using Babel, e.g., those not using JSX syntax.

First, let’s install the Babel CLI:

npm i babel-cli -D

Now add a transpile script which we will use to cause Babel to transpile our component source files, as well as copy any non-source assets (e.g., .css files), into a target folder named dist.

At the same time, let’s set the main entry point of our component to the transpiled version.

Make the following modifications to package.json:

{
"name": "my-component",
"version": "1.0.0",
"description": "",
"main": "dist/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "webpack-dev-server --mode development",
"transpile": "babel src -d dist --copy-files"
},
"author": "",
"license": "ISC",
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-core": "^6.26.0",
"babel-loader": "^7.1.4",
"babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.24.1",
"css-loader": "^0.28.11",
"html-webpack-plugin": "^3.2.0",
"react": "^16.3.2",
"react-dom": "^16.3.2",
"style-loader": "^0.20.3",
"webpack": "^4.5.0",
"webpack-cli": "^2.0.14",
"webpack-dev-server": "^3.1.3"
}
}

Try it out:

npm run transpile

There should now be a dist folder in the project root, containing a transpiled version of index.js, along with a copy of styles.css. These are the ready-to-use files that a user can import and use in their project.

Let’s simplify our workflow by adding a prepublishOnly script. This script will be run automatically by npm every time we publish our component. This ensures that we always publish the latest transpiled version of our component.

At the same time, let’s specify that we expect target projects using our component to have a specific version of React installed. Expressing this as a peerDependency causes npm to not include React in the published package, which reduces package size and avoids the disaster of having multiple versions of React in the user’s target project.

Make the following modifications to package.json:

{
"name": "my-component",
"version": "1.0.0",
"description": "",
"main": "dist/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "webpack-dev-server --mode development",
"transpile": "babel src -d dist",
"prepublishOnly": "npm run transpile"

},
"author": "",
"license": "ISC",
"peerDependencies": {
"react": "^16.3.0",
"react-dom": "^16.3.0"
},
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-core": "^6.26.0",
"babel-loader": "^7.1.4",
"babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.24.1",
"css-loader": "^0.28.11",
"html-webpack-plugin": "^3.2.0",
"react": "^16.3.1",
"react-dom": "^16.3.1",
"style-loader": "^0.20.3",
"webpack": "^4.5.0",
"webpack-cli": "^2.0.14",
"webpack-dev-server": "^3.1.3"
}
}

Finally, let’s inform npm that there are files and folders in our project that can be omitted from the published package. Add .npmignore to the project root.

# .npmignore 
src
examples
.babelrc
.gitignore
webpack.config.js

We are now ready to publish our component to npm:

npm publish

When you browse your profile on npm, you should see the new package. Your component is shipped!

Let’s set up the last workflow.

Publish an online demo to GitHub Pages

Hosting our online demo on GitHub Pages is free. It requires using Webpack to build a Production version, and then pushing it to a special branch in our GitHub repo. Let’s automate this.

First, install a helper package which will maintain the special GitHub branch for us. Don’t worry that we haven’t added Git source control to our project yet; we will get to that in a moment.

npm i gh-pages -D

Now let’s add three new scripts to package.json:

{
"name": "my-component",
"version": "1.0.0",
"description": "",
"main": "dist/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "webpack-dev-server --mode development",
"transpile": "babel src -d dist --copy-files",
"prepublishOnly": "npm run transpile",
"build": "webpack --mode production",
"deploy": "gh-pages -d examples/dist",
"publish-demo": "npm run build && npm run deploy"

},
"author": "",
"license": "ISC",
"peerDependencies": {
"react": "^16.3.0",
"react-dom": "^16.3.0"
},
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-core": "^6.26.0",
"babel-loader": "^7.1.4",
"babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.24.1",
"css-loader": "^0.28.11",
"gh-pages": "^1.1.0",
"html-webpack-plugin": "^3.2.0",
"react": "^16.3.2",
"react-dom": "^16.3.2",
"style-loader": "^0.20.3",
"webpack": "^4.5.0",
"webpack-cli": "^2.0.14",
"webpack-dev-server": "^3.1.3"
}
}

The build script causes Webpack to build a bundled, minified Production version of our demo, but we need to tell Webpack where to put the resulting files:

/*** webpack.config.js ***/
const path = require('path');
const HtmlWebpackPlugin = require("html-webpack-plugin");
const htmlWebpackPlugin = new HtmlWebpackPlugin({
template: path.join(__dirname, "examples/src/index.html"),
filename: "./index.html"
});
module.exports = {
entry: path.join(__dirname, "examples/src/index.js"),
output: {
path: path.join(__dirname, "examples/dist"),
filename: "bundle.js"
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
use: "babel-loader",
exclude: /node_modules/
},
{
test: /\.css$/,
use: ["style-loader", "css-loader"]
}
]
},
plugins: [htmlWebpackPlugin],
resolve: {
extensions: [".js", ".jsx"]
},
devServer: {
port: 3001
}
};

Let’s give it a try:

npm run build

You will find the Production build in examples/dist.

Now let’s add Git source control to our project. Start by adding a .gitignore file in the project root, which we use to exclude non-source files from source control:

# .gitignore
node_modules
dist

Go to GitHub and create a new repository for your project. From the screen that subsequently appears, copy and run the commands under the heading ...or create a new respository on the command line, which will initialize a local Git repo and connect it to the remote GitHub repo.

Now that our local and remote repos are created and linked, we are ready to set up the demo hosting environment. This requires configuring our GitHub repo to look for our demo build in a branch named gh-pages. Our deploy script uses the gh-pages package to perform this setup for us, and to maintain the branch contents. Let’s test it out:

npm run deploy

Click the Settings link in your repository in GitHub and scroll down to the GitHub Pages section. You should see a link to your demo. Your demo is online!

Finally, the publish-demo script we added simplifies our workflow even further by chaining the build and deploy scripts together into a single script:

npm run publish-demo

Wrapping up

Whenever you are ready to publish a new version, simply increment the version in package.json, and then run npm publish and npm run publish-demo. Publishing a new version to npm is effective immediately, while publishing a new demo version to GitHub Pages can take 20 minutes or so to become effective.

There are other things you will want to do to complete your component’s community presence, which are beyond the scope of this article. Here are a few to get you going:

  • Develop your project’s README.md: add a description of the component, a link to the demo, example usage, and specify the component’s API
  • Add automated tests
  • Populate the description and repository fields in package.json
  • Consider what license you will offer with your component

Thanks for reading!