React is the most popular frontend web development library in use today. However, there’s a problem. Competing frameworks undermine what could be a ubiquity—React, as far as the eye can see, an ever-flowing landscape context and declarative UI. This is the dream of React developers. All awaiting the day our awe-inspiring idea becomes reality. Well, rejoice, for the day is here! I have come before you to lay the groundwork for fulfilling our vision.
How can that be? I hear you whisper, a joyful tear undoubtedly in your eye. This is possible with a library called React to Web component (@r2wc/react-to-web-component) and a powerful bundling tool. Before getting into the technical drudgery of implementing this most joyous of solutions, let’s share a vision.
Your Angular app? React. Your Vue app? React. Have a vanilla JS app? Not anymore. React.
But what about the issues SEO and performance with Re- shhh shhh. Not now.
But since react-to-webcomponent leverages web components, doesn’t this show that web components are better at– shhh again—no more questions.
Together, in this post, we will create a multi-entry component library that allows us to use React everywhere. There will not be details on how to create each component; rather, we will focus on the build tooling, @r2wc/react-to-web-component
, and Vite. Once built, we will demonstrate the power of this glorious component library in two projects—a React one and an Angular one—to show how it can be used ever to spread the influence and ubiquity of React.
Let’s Drop the Act
Let’s address something before moving on… Should you do this? short answer, No. I like React, but it has plenty of issues. React isn’t always the best solution, especially if your team already uses a different library or framework (If they use Angular, then boy, do I have a great blog site for you).
This article aims to show some interesting things you can do with Vite via @r2wc/react-to-web-component
. You know that quote from Jeff Goldblum in Jurassic Park:
Your scientists were so preoccupied with whether or not they could that they didn’t stop to think if they should.
I want to answer that question right away. You shouldn’t; however, we can learn interesting concepts by doing things we shouldn’t (within limitations…). That’s what this post aims to do. We will go in-depth on Vite and npm to learn how to create a multi-entry component library. The only way for this to go well is if we all, you and I, dear reader, agree not to do this outside this post. It's a great learning exercise, but shouldn’t be used in a production environment. Now that we have both agreed, we can get back to the technical details of this post.
Humble Beginnings
We can lay the groundwork for our library by running the CLI commands provided by Vite and answering some prompts.
npm create vite@latest
In our case, we want React and TypeScript. In the coming sections, we will modify the vite.config.ts
file out of the box; it has the react
plugin.
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()]
})
By default, the Vite React version of the CLI bootstraps a React application, which is helpful, but we’re going to create a component library. Because of this, we need to do some cleaning up and modifying to set up the project for “lib mode.”
To do this, first, remove the index.html
file from the project. By default, Vite uses this as the entry point; since we’re creating a library, there's no need for it. Feeling lazy and just want a command to run? I respect that; hopefully, you have a mac. Here you go:
rm src/index.html
In a similar vein, we can also remove the public
folder, which is where we would put our static assets.
rm -rf public
We’ve gotten rid of all we need to, but in the process have removed the entry point to the project, which is important. We need to add the correct entry point to our project.
A Brief Dive into Architecture
As stated above, we’ll skip over the React component creation; however, we must understand the project's high-level structure since we are focusing on the build system.
Don’t want to read about it? You’d rather see it? You and I grow closer every second. Here’s a link to the repo.
This component library will have a flat structure containing modlets for each component. If you’re unfamiliar with modlets, it's a fantastic folder structure that prioritizes collocation and the principle of single responsibility; you can read more about them here. All the components will be exported from a single index.ts
file at the top of the src/
folder.
└── src
└── Header
└── index.ts
Back to the Show
We can modify our vite.config.ts
file with this knowledge to support our component library. Vite provides a way for developers to set up a library's build through the build.lib
option in the config.
While mucking about in the configuration file, we need to add some TypeScript support. Vite supports TypeScript out of the box but doesn’t bundle types. We need to include a plugin to add the declaration files to our build. We can install the needed dependencies using these commands.
npm i -D @rollup/plugin-typescript tslib
Then we can update our Vite config to use the plugin and define our entry points.
An Aside on Modules
In the next bit, we will be telling Vite which module types to create for our projects. In React development, we only need ES modules; however, since we’re breaking rules and doing things we shouldn’t be for pedagogical purposes, let’s bundle multiple modules. We will create an es
module and a umd
module. The difference being you can import
an es
module but not require
it, as opposed to a umd
module which you can require
and import
.
Back to Configuration
With that, let’s update our vite.config.ts
.
import { resolve } from 'path'
import { defineConfig } from 'Vite'
import react from '@Vitejs/plugin-react'
import typescript from '@rollup/plugin-typescript'
export default defineConfig(() => ({
plugins: [react(), typescript()],
build: {
lib: {
formats: ['es', 'umd'],
entry: resolve(__dirname, 'src/index.ts'),
name: 'index',
fileName: (format) => `index.${format}.js`,
},
rollupOptions: {
external: ['react'],
output: {
globals: {
react: 'react',
},
},
},
},
}))
We also need to ensure our TypeScript configuration file outputs the project’s type declaration to the correct folder, which in our case, is /dist
.
// Comments don't exist in JSON... Let's pretend they do to keep this short.
{
"compilerOptions": {
// ... rest of compiler options
"outDir": "./dist",
"declaration": true
},
// ... rest of config
}
With that, our build is ready; if we run npm run build
, some logs appear in the console, and after a /dist
folder with our bundle and types appear in our file system. Now, we need to update the project’s package.json
file.
Prepare the Package
There are four fields in package.json
to be familiar with before updating it in our project. Below is a brief definition of each.
Field |
Description |
---|---|
|
|
|
|
|
|
|
Looking at the list, exports
and main
seem to do the same thing; to be fair, they do; however, as we will see later on, exports
give us more control. It is a newer npm feature, so we need to include both to account for older projects.
{
// ...
"main": "./dist/index.umd.js",
"module": "./dist/index.es.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.es.js",
"require": "./dist/index.umd.js"
}
},
"files": [
"dist/",
"src/",
"!src/**/*.test.ts"
],
// ...
}
With that, the package is useable, and we can see it in action by linking it to a project locally–
So what? I hear you scream. You promised a world of React domination. This is just a component library. Where’s the magic? Get on with it already.
I hear you (metaphorically, of course), and we will get there. Your patience will be worth it. We first need to show the React component library works with React.
When using this component library while writing this post, I am installing the component library from git to not pollute npm. It should be publically available if you want to try it elsewhere!
After installing and building, we can use our Header
component in our app like so:
import { Header } from "vite-react-to-webcomponent";
function App() {
return <Header text="React" />;
}
and it renders a such (you can see it live here):
Your patience was worth it. We have laid the groundwork needed to build a much more powerful library. Previously, I mentioned @r2wc/react-to-web-component
would assist Vite, now is the time to meet the show's star.
The Star Takes the Stage
The keystone of this project’s industry-redefining capabilities is @r2wc/react-to-web-component
, which converts React components to custom elements. It lets you share React components as native elements that don't require being mounted through React.
The custom element acts as a wrapper for the underlying React component. You can use these custom elements with any project that uses HTML the same way you would standard HTML elements.
Under the hood, @r2wc/react-to-web-component
creates a CustomElementConstructor
with custom getters/setters and life cycle methods that keep track of the props that you have defined. When a property is set, its custom setter:
-
Re-renders the React component inside the custom element.
-
Syncs the property with the web component's attributes so that whenever that property is requested, it can be returned to you.
For a more in-depth look at @r2wc/react-to-web-component
, check out the API docs. Also, be sure to check out this blog post if you want to use react-to-webcomponent
with Create React App instead of Vite.
Adding Web Component Support
@r2wc/react-to-web-component
can be installed using this command:
npm i @r2wc/react-to-web-component
We can then add it to our index.ts
file at the root of our project and create web components for all of the React components we are exporting.
// index.ts
export * from "./Button";
export * from "./Header";
customElements.define(
"rwc-header", r2wc(Header, {
props: { text: "string" },
})
);
This is not our final implementation, there are some issues with this approach (which we will discuss below), but with this, our radical new component library is ready for use in other projects.
In Angular
For the Angular project, I’m just going to gut the starter app from the Angular docs, you can find out how that all works here.
We will need to enable web component support on the Angular project to use our component library in an Angular app. To do this, you can read the docs or just navigate to the app.module.ts
file and include the CUSTOM_ELEMENT_SCHEMA
in the schemas
section of the @NgModule
// app.module.ts
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule],
providers: [],
bootstrap: [AppComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class AppModule {}
Now we can import our component library in app.component.ts
import 'vite-react-to-webcomponent'
and use our react components as web components in the app.component.html
file
<rwc-header text="Angular"></rwc-header>
With these few lines of code in place, when we run the angular project, we will see our components (or you can see it live here).
With this we have achieved our goal of creating a component library that will foster a golden age of web development. There are some issues with this approach, though. Regarding bundles, there is no distinction between the web components and the React components. This means our:
import 'vite-react-to-webcomponent'
brings in all the web components and all the react components, even though we really don’t need any of the React components. Let’s fix this.
Multiple Entry Points
We can address this issue by separating the web components and the React components. We can use Vite to do this; however, it will require us to move away from the vite.config.ts
file and create our build programmatically since vite.config.ts
does not currently support multi-entry builds. We can get around this using the Vite JS API and calling build
more than once. We also need to move our react-to-webcomponent
logic into a separate file. We can use the new file as the entry point for our web component build.
Let’s start with the new file. At the top level of the component library, we can create a webcomponents.ts
file and move the react-to-webcomponent
logic there.
import r2wc from "@r2wc/react-to-web-component";
import { Button } from "./Button";
import { Header } from "./Header";
customElements.define(
"rwc-header",
r2wc(Header, {
props: { text: "string" },
})
);
Our current Vite config file can be re-written as a node script like so:
// scripts/build.js
import { build, defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import typescript from "@rollup/plugin-typescript";
import cssInjectedByJsPlugin from "vite-plugin-css-injected-by-js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const reactComponentLibrary = {
plugins: [],
entry: path.resolve(__dirname, "../src/index.ts"),
fileName: (format) => `index.${format}.js`,
name: "index",
};
const getConfiguration = ({ plugins, ...library }) => {
return defineConfig(() => ({
plugins: [react(), typescript(), cssInjectedByJsPlugin(), ...plugins],
build: {
lib: {
formats: ["es", "umd"],
...library,
},
},
rollupOptions: {
external: ["react"],
output: {
globals: {
react: "react",
},
},
},
}));
};
const viteBuild = (configFactory) => {
const config = configFactory();
return build(config);
};
const buildLibraries = async () => {
await viteBuild(getConfiguration(reactComponentLibrary));
};
buildLibraries();
Rather than doing it all inside the buildLibraries
function, some of the logic has been pre-abstracted to make adding another build step easier. Looking at the code, we now need to add another library configuration and make the same calls.
// same imports as above...
// ... theres a lot of them... makes it hard to read... you get it
const reactComponentLibrary = {
plugins: [],
entry: path.resolve(__dirname, "../src/index.ts"),
fileName: (format) => `index.${format}.js`,
name: "index",
};
const webcomponentsLibrary = {
plugins: [],
entry: path.resolve(__dirname, "../src/webcomponents.ts"),
fileName: (format) => `webcomponents.${format}.js`,
name: "webcomponents",
};
// same things as before
const buildLibraries = async () => {
await Promise.all(
[webcomponentsLibrary, reactComponentLibrary]
.map(getSharedConfiguration)
.map(viteBuild)
);
};
buildLibraries()
This creates our build, but our package isn’t informed of our new entry points; our final step is to update the package.json
to include our new build.
{
//...
"exports": {
".": {
"import": "./dist/index.es.js",
"require": "./dist/index.umd.js",
"types": "./dist/index.d.ts"
},
"./webcomponents": {
"import": "./dist/webcomponents.es.js",
"require": "./dist/webcomponents.umd.js",
"types": "./dist/webcomponents.d.ts"
}
},
scripts: {
// ...
"build": "node scripts/build.js"
}
// ...
}
Returning to the angular application, we just need to now update the import to go to /webcomponent
import 'vite-react-to-webcomponent/webcomponents'
Great. Now What?
With this, we’re done. The sun sets on a peaceful world as React wipes out any competing frameworks and libraries. People often say it's not about the destination but the journey; is that the case with this powerful component library? Absolutely, but that's not the point. If we shouldn’t go out and try to turn the world into a glorious React-filled empire, what should we walk away from this with?
There’s plenty we can walk away from this with. @r2wc/react-to-web-component
is a cool library, and you should check it out! Just don't abuse it! Build tooling has a lot more depth than most developers give it credit, and we, as developers, could give them a little more love.
Head over to our CodeSandbox example to play around some more!
Don’t Over React
Need help with your React project? We have a team of experts in React consulting who are eager to help! Schedule your free consultation call to pick our brains and get started. You can also ask questions or start a conversation on our Community Discord!
Oh… You’re still here?
We’ve talked a lot and have mentioned a ton of repos. Wouldn’t it be great if there was a list? I’ve got you.