Home
Projects
Certificates
Articles
Social
AMWP
Home
01
Projects
02
Certs
03
Articles
04
Social
05
© 2026 Afif Medya
Back to Writings
article
August 15, 2026
React and Vanilla JS using Web Components & Vite!
# Bridging React and Vanilla JS: Building a Micro-Frontend with Web Components and Vite When building a lightweight website, rendering HTML on the server (using EJS, Pug, or even plain PHP) is often the fastest and most SEO-friendly approach. It avoids the overhead of shipping megabytes of JavaScript just to render static text. But what happens when you need to integrate a highly complex, interactive component that relies on a specific framework ecosystem? In my portfolio, I use raw **EJS** and **Vanilla JS** for page loads. However, I wanted to implement a live Discord status tracker (Lanyard) that heavily relies on React hooks and WebSocket state management. Rewriting my entire portfolio in React/Next.js just for one component was unacceptable. The solution? **Web Components and Vite.** Here is how I wrapped a full React application inside a native HTML Web Component, creating a seamless Micro-Frontend architecture. --- ## The Challenge: Ecosystem Lock-in The `react-use-lanyard` hook is fantastic. It handles WebSocket connections to Discord, auto-reconnects, and manages state effortlessly. But, as the name implies, it only works in React. If you have a plain HTML file, you can't just `<script src="react-lanyard.js">` and expect it to magically render. We needed a bridge between the React world and the Vanilla DOM. ## Enter Web Components (Custom Elements) Web Components are a browser-native API that allows developers to create custom HTML tags (like `<my-component>`). They encapsulate their own styling and functionality, acting as an isolated "black box" that the rest of the page doesn't need to understand. By turning our React app into a Web Component, we can use it in our EJS files like this: ```html <!-- Inside our plain EJS/HTML file --> <react-lanyard discord-id="1234567890"></react-lanyard> ``` --- ## Step 1: The React Wrapper First, we created a standard React component (`App.jsx`) that fetches and displays the Discord presence using the Lanyard API. The magic happens in `main.jsx`, where instead of attaching React directly to a hardcoded `<div id="root">`, we define a `CustomElement`: ```javascript import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App.jsx'; import './Lanyard.css'; // Tailwind & Custom CSS class ReactLanyardElement extends HTMLElement { connectedCallback() { // Read the Discord ID passed via HTML attributes const discordId = this.getAttribute('discord-id') || "default-id"; // Create an isolated shadow root or just use the element itself const mountPoint = document.createElement('div'); this.appendChild(mountPoint); // Mount the React Application inside the Custom Element! const root = ReactDOM.createRoot(mountPoint); root.render( <React.StrictMode> <App discordId={discordId} /> </React.StrictMode> ); } } // Register the custom HTML tag with the browser if (!customElements.get('react-lanyard')) { customElements.define('react-lanyard', ReactLanyardElement); } ``` When the browser parses `<react-lanyard>` in our HTML, it triggers `connectedCallback()`, reads the attributes, and seamlessly boots up the React application inside that specific DOM node. --- ## Step 2: Bundling with Vite React projects usually spit out multiple chunk files and CSS files. To make our Web Component portable, we needed Vite to bundle everything into a **single, standalone JavaScript file**. We tweaked `vite.config.js` to disable CSS extraction and code-splitting: ```javascript import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'; export default defineConfig({ plugins: [ react(), // This plugin forces all CSS to be injected into the JS bundle cssInjectedByJsPlugin() ], build: { rollupOptions: { output: { // Prevent Vite from splitting code into vendor chunks manualChunks: undefined, // Force the output file to always have the same name entryFileNames: 'react-lanyard-bundle.js', }, }, }, }); ``` When we run `npm run build`, Vite gives us a single `react-lanyard-bundle.js` file containing React, our code, the Lanyard dependencies, and the Tailwind CSS styling. --- ## Step 3: Deployment & Integration The integration step is now incredibly simple. We just copy `react-lanyard-bundle.js` to the `public/js` folder of our Vanilla Node.js backend. Inside our EJS template, we simply include the script and use our new custom tag: ```html <!-- Load our bundled React Web Component --> <script type="module" src="/js/react-lanyard-bundle.js"></script> <!-- Use it anywhere in the DOM --> <div class="glass-card"> <h3 class="font-bold">Live Discord Status</h3> <!-- It behaves exactly like a native HTML element! --> <react-lanyard discord-id="761858712128798721"></react-lanyard> </div> ``` --- ## Conclusion By bridging React and Vanilla JavaScript with Web Components, we achieved the ultimate "best of both worlds": - **Fast Loads**: The core website remains lightweight, server-rendered EJS. - **Micro-Frontend Flexibility**: We can use complex React hooks and state management strictly where it is needed. - **Portability**: The `<react-lanyard>` component can now be dropped into a WordPress site, a PHP project, or any other plain HTML file without touching Webpack or Babel. If you ever feel stuck choosing between "Vanilla Speed" and "Framework Ecosystems," remember that Web Components can effortlessly bridge the gap.
Table of Contents