Skip to main content
imvinojanv.dev
  • About
  • Blog
  • Projects
  • Snippets

Command Palette

Search for a command to run...

imvinojanv.dev
Open to Hire

I'm always open to discussing software engineering work or partnership.

HomeAboutResumeUses
BlogProjectsSnippets
ยฉ 2026 Vinojan Veerapathirathasan โ€”โ€” Colombo, Sri Lanka.
MediumGitHubLinkedInRSS
Back

Seamless React Component Injection: Build Once, Inject Anywhere!

A tutorial on packaging a React project as a web component and injecting it into another React project using JS/CSS bundles.

Published on March 21, 2025
7 min read
Seamless React Component Injection: Build Once, Inject Anywhere!

Hi there ๐Ÿ‘‹,

Welcome back to another exciting journey into the modern web development landscape. Today, weโ€™re diving deep into the realm of React project injection using Web Components. ๐Ÿš€

๐Ÿ“‘ In this tutorial, weโ€™ll explore how to create a reusable React component as a web component (Project A), build it into JS and CSS files, and dynamically inject it into another React project (Project B). This approach will enable you to enhance modularity, simplify maintenance, and improve scalability in your React applications. Are you ready to take your React skills to the next level? Letโ€™s get started! ๐ŸŽ‰

1. Create Project A

Step 1: Create a New React Project

You can create a new React project using the following command:

npx create-react-app project-a

Step 2: Write the Main Component (App.js)

Create a simple component for Project A that will be injected into Project B.

src/App.jsx
import React from "react";
 
const App = ({ eventId, version }) => {
  return (
    <div>
      <h2>Project A</h2>
      <p>Event ID: {eventId}</p>
      <p>Version: {version}</p>
    </div>
  );
};
 
export default App;

Step 3: Define a Web Component in index.js

Use customElements.define() to create a web component that can be mounted into Project B.

src/index.js
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
 
// Function to define the web component
export function defineWebComponent(name, Component) {
  if (!customElements.get(name)) {
    // Project A is exposed as a web component and made it injectable
    customElements.define(
      name,
      class extends HTMLElement {
        constructor() {
          super();
 
          // Attach Shadow DOM
          if (!this.shadowRoot) {
            this.attachShadow({ mode: "open" });
          }
 
          let container = this.shadowRoot.querySelector(".custom-dashboard");
          if (!container) {
            container = document.createElement("div");
            container.className = "custom-dashboard";
            this.shadowRoot.appendChild(container);
          }
          this.root = ReactDOM.createRoot(container);
 
          this.eventId = null;
          this.version = "";
        }
 
        connectedCallback() {
          this.renderComponent();
        }
 
        disconnectedCallback() {
          this.root.unmount();
        }
 
        // Initialize method to be called by the host website
        init({ eventId, version }) {
          this.eventId = eventId;
          this.version = version.replace("v", "");
 
          const styleId = `custom-dashboard-style`;
 
          // Load CSS into Shadow DOM if not already loaded
          const existingLink = this.shadowRoot.getElementById(styleId);
          if (!existingLink) {
            const link = document.createElement("link");
            link.rel = "stylesheet";
            link.href = `${process.env.REACT_APP_CSS_BASE_URL}/main.css`; // Use your built js/css storage bucket URL in .env
            link.id = styleId;
 
            link.onload = () => console.log(`CSS loaded: ${link.href}`);
            link.onerror = () =>
              console.error(`Failed to load CSS: ${link.href}`);
 
            this.shadowRoot.appendChild(link);
          }
 
          // Render component after initialization
          this.renderComponent();
        }
 
        renderComponent() {
          if (!this.eventId || !this.version) {
            this.root.render(null);
            return;
          }
 
          // Render the React component
          this.root.render(
            <App eventId={this.eventId} version={this.version} />,
          );
        }
      },
    );
  }
}
 
// Define the custom element
defineWebComponent("custom-dashboard", App);
 
// Uncomment below this codes for the local development
 
// const rootElement = document.getElementById("root");
// const renderDOM = ReactDOM.createRoot(rootElement);
 
// const eventId = "bet";
// const version = "1.0";
 
// renderDOM.render(
//     <App eventId={this.eventId} version={this.version} />
// );
REACT_APP_CSS_BASE_URL=https://example-bucket.s3.amazonaws.com

2. Build Project A into JS and CSS Files

You need to modify the build scripts to generate JavaScript and CSS files that can be imported into another project.

Step 1: Add Custom Build Script in package.json

Open package.json and modify the scripts section:

package.json
"scripts": {
  "start": "react-scripts start",
  "build": "react-scripts build && node scripts/postbuild.js"
}

Step 2: Create a postbuild.js File

Create a scripts/postbuild.js file to copy the build files to a storage bucket or a specific folder:

scripts/postbuild.js
const fs = require("fs");
const path = require("path");
 
const buildPath = path.resolve(__dirname, "../build");
const outputPath = path.resolve(__dirname, "../dist");
 
if (!fs.existsSync(outputPath)) {
  fs.mkdirSync(outputPath);
}
 
// Copy JS and CSS files
const files = fs.readdirSync(buildPath);
files.forEach((file) => {
  if (file.endsWith(".js") || file.endsWith(".css")) {
    fs.copyFileSync(path.join(buildPath, file), path.join(outputPath, file));
    console.log(`Copied: ${file}`);
  }
});
 
console.log("Build files copied to dist folder.");

Step 3: Build the Project

Build the project using:

npm run build

After building, the dist or folder will contain the .js and .css files:

Project Structure
root/
โ”œโ”€โ”€ build/           # Full React build output
โ”‚   โ”œโ”€โ”€ static/
โ”‚   โ”œโ”€โ”€ index.html
โ”‚   โ”œโ”€โ”€ main.js
โ”‚   โ””โ”€โ”€ main.css
โ”œโ”€โ”€ dist/            # Cleaned files for embedding
โ”‚   โ”œโ”€โ”€ main.js
โ”‚   โ””โ”€โ”€ main.css
โ”œโ”€โ”€ scripts/
โ”‚   โ””โ”€โ”€ postbuild.js
โ””โ”€โ”€ src/
    โ”œโ”€โ”€ index.js
    โ””โ”€โ”€ App.jsx

You should use the files inside the dist folder (main.js and main.css) for the injection. because its Clean, Minified, and Optimized for performance.

Step 4: Upload to Storage Bucket (Optional)

You can upload these files to an S3 bucket or any public CDN. Or else you can set up a CICD pipeline to build the project and automatically store those files into a storage bucket.

https://example-bucket.s3.amazonaws.com

3. Create Project B

Step 1: Create a New React Project

Create another React project where Project A will be injected:

npx create-react-app project-b

Step 2: Add Injection Logic in Project B

Create a component to load the JS and CSS files dynamically and initialize the web component.

src/components/ProjectAPreview.jsx
import React, { useEffect, useState } from "react";
 
const ProjectAPreview = () => {
  const [reloadKey, setReloadKey] = useState(1);
  const [show, setShow] = useState(false);
 
  const selectedVersion = "1.0";
  const baseUrl = "https://example-bucket.s3.amazonaws.com"; // Put it on n.env file
 
  // Component reloading and cleanup
  const handleReload = () => {
    console.log("Reloading...");
 
    setShow(false);
    removeScript(`${baseUrl}/main.js`);
    removeStylesheet(`${baseUrl}/main.css`);
 
    injectStylesheet(`${baseUrl}/main.css`);
    injectScript(`${baseUrl}/main.js`);
 
    setReloadKey((prevKey) => prevKey + 1);
 
    setTimeout(() => {
      initProjectA();
      setShow(true);
    }, 1000);
  };
 
  useEffect(() => {
    setShow(false);
 
    injectStylesheet(`${baseUrl}/main.css`);
    injectScript(`${baseUrl}/main.js`);
 
    setTimeout(() => {
      initProjectA();
      setShow(true);
    }, 1000);
 
    return () => {
      removeScript(`${baseUrl}/main.js`);
      removeStylesheet(`${baseUrl}/main.css`);
    };
  }, [reloadKey, selectedVersion]);
 
  const initProjectA = () => {
    console.log("Initializing Project A with version:", selectedVersion);
 
    const dashboard = document.querySelector("custom-dashboard");
    if (dashboard?.init) {
      dashboard.init({
        eventId: "project-a-event",
        version: selectedVersion,
      });
    }
  };
 
  const injectScript = (src) => {
    const script = document.createElement("script");
    script.src = src;
    script.async = false;
    script.onload = () => console.log(`Script loaded: ${src}`);
    document.body.appendChild(script);
  };
 
  const removeScript = (src) => {
    const existingScript = document.querySelector(`script[src="${src}"]`);
    if (existingScript) {
      existingScript.remove();
      console.log(`Script removed: ${src}`);
    }
  };
 
  const injectStylesheet = (href) => {
    const existingLink = document.querySelector(`link[href="${href}"]`);
    if (!existingLink) {
      const link = document.createElement("link");
      link.rel = "stylesheet";
      link.href = href;
      link.onload = () => console.log(`CSS loaded: ${href}`);
      document.head.appendChild(link);
    }
  };
 
  const removeStylesheet = (href) => {
    const existingLink = document.querySelector(`link[href="${href}"]`);
    if (existingLink) {
      existingLink.remove();
      console.log(`CSS removed: ${href}`);
    }
  };
 
  return (
    <div key={reloadKey}>
      <div>
        <button onClick={handleReload}>Reload Project</button>
      </div>
      <div>
        {!show ? (
          <p>Initializing...</p>
        ) : (
          <custom-dashboard style={{ width: "100%" }}></custom-dashboard>
        )}
      </div>
    </div>
  );
};
 
export default ProjectAPreview;

Here I am passing the data (eventId and version) from Project B to Project A using the init() method, this is more like prop passing or data injection.

Step 3: Import the ProjectAPreview to App.js

src/App.js
import React from "react";
import ProjectAPreview from "./components/ProjectAPreview";
 
const App = () => {
  return (
    <div>
      <h2>This is Project B</h2>
      <ProjectAPreview />
    </div>
  );
};
 
export default App;

4. Run and Test

Step 1: Start Project B

npm start

Step 2: Test the Injection

  • Open your browser at http://localhost:3000.
  • The Project A component should be injected into Project B.
  • Click the Reload Project button to reload it dynamically.

This approach allows you to encapsulate a React-based project into a standalone component that can be injected into any other React project (or even non-React projects) using plain JavaScript. Itโ€™s lightweight, modular, and easy to maintain! ๐Ÿ˜Ž


I appreciate you taking the time to read this article.๐Ÿ™Œ

Before you move on to explore the next article, donโ€™t forget to give your claps ๐Ÿ‘ for this article and share with your friends. Stay connected with me on social media. Thanks for your support and have a great rest of your day! ๐ŸŽŠ

โœ๏ธ Vinojan Veerapathirathasan.

0
0
0
0
0

On This Page

1. Create Project AStep 1: Create a New React ProjectStep 2: Write the Main Component (`App.js`)Step 3: Define a Web Component in `index.js`2. Build Project A into JS and CSS FilesStep 1: Add Custom Build Script in `package.json`Step 2: Create a `postbuild.js` FileStep 3: Build the ProjectStep 4: Upload to Storage Bucket (Optional)3. Create Project BStep 1: Create a New React ProjectStep 2: Add Injection Logic in Project BStep 3: Import the `ProjectAPreview` to App.js4. Run and TestStep 1: Start Project BStep 2: Test the Injection
Last updated: March 21, 2025