<img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1%20https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1"> Bitovi Blog - UX and UI design, JavaScript and Front-end development

React Everywhere with Vite and React-to-Webcomponent

David Nicholas

Use React anywhere with Vite and react-to-webcomponent. You'll build a multi-entry component library to deploy react anywhere using react-to-webcomponent!

posted in React, Angular on December 13, 2022 by David Nicholas


React Everywhere with Vite and React-to-Webcomponent

David Nicholas by David Nicholas

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-webcomponent and a powerful bundling tool. Before getting into the technical drudgery of implementing this most joyous of solutions, let’s share a vision.

everything the light touches is react

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, react-to-webcomponent, 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 react-to-webcomponent. You know that quote from Jeff Goldblum in Jurassic Park:

jeff-goldblum-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

exports

exports enable us to define the entry points of the package when imported from node_modules.

main

main is another way to define the entry point of the package.

files

files instruct npm on which files to include in the package.

types

types is a way to include our type declarations in our package.

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):

react app

Your patience was worth it. We have laid the groundwork needed to build a much more powerful library. Previously, I mentioned react-to-webcomponent 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 react-to-webcomponent, 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, react-to-web-component creates a constructor function whose prototype is a Proxy. This acts as a trap for any property set on instances of the custom element. When a property is set, the proxy:

  • Re-renders the React component inside the custom element.

  • Creates an enumerable getter/setter on the instance to save the set value and avoid hitting the proxy in the future.

For a more in-depth look at react-to-webcomponent, 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

react-to-webcomponent can be installed using this command:

npm i react-to-webcomponent

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",
  reactToWebComponent(Header, React, ReactDOMClient, {
    props: ["text"],
  })
);

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.

r2wc-haboob

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).

components

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 React from "react";
import * as ReactDOMClient from "react-dom/client";
import reactToWebComponent from "react-to-webcomponent";

import { Button } from "./Button";
import { Header } from "./Header";

customElements.define(
  "rwc-header",
  reactToWebComponent(Header, React, ReactDOMClient, {
    props: ["text"],
  })
);

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. React-to-webcomponent 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.

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

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.

Create better web applications. We’ll help. Let’s work together.