React, but with Web Components
Posted by Dustin Boston on in Web Development.
Did you know you can replicate a component-based architecture similar to React without any libraries or build systems? By using native web components, you can build powerful applications without external dependencies. This approach is a surprisingly novel solution. Here’s how it works.
index.html
Loads the scripts and renders the data.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Netflix</title>
</head>
<body>
<netflix-gallery id="gallery"></netflix-gallery>
<script type="module">
import render from "./scripts/app.js"; // Builds the application
import data from "./scripts/data.js"; // Loads the data
import "./scripts/components/index.js"; // Registers components
document.addEventListener("DOMContentLoaded", function (event) {
render(data);
});
</script>
</body>
</html>
app.js
Maps data into a more performant structure and initializes the gallery.
import Video from "./models/Video.js";
import Row from "./models/Row.js";
function mapDataToRows(data) {
// Map videos and billboards ids to their indexes for faster lookups
const videoMap = new Map();
// ...map videos
const billboardMap = new Map();
// ...map billboards
const rows = data.rows.map((r, i) => {
const type = "video";
const buttons = [];
const videos = [];
// ...transform data
return new Row(type, videos, buttons);
}, []);
return rows;
}
export default function render(data) {
const rows = mapDataToRows(data);
const gallery = document.getElementById("gallery");
if (gallery) {
gallery.rows = rows;
}
}
scripts/components/index.js
Defines all of the web components that will be used within the application.
import VideoComponent from "./Video.js";
import VideoRowComponent from "./VideoRow.js";
import HeaderRowComponent from "./HeaderRow.js";
import InlineRowComponent from "./InlineRow.js";
import GalleryComponent from "./Gallery.js";
window.customElements.define("netflix-video", VideoComponent);
window.customElements.define("netflix-video-row", VideoRowComponent);
window.customElements.define("netflix-inline-row", InlineRowComponent);
window.customElements.define("netflix-header-row", HeaderRowComponent);
window.customElements.define("netflix-gallery", GalleryComponent);
Now that we’ve set up the application, let’s take a look at a few key components.
scripts/components/Gallery.js
The Gallery is the main view in the application. It displays rows of images where each row can potentially be a different type.
let rows;
let viewportHeight;
let preloadHeight;
let lastRenderedRow;
let debounceTimeoutId;
class GalleryComponent extends HTMLElement {
set rows(data) {
rows = data;
this.render();
}
constructor() {
super();
rows = [];
viewportHeight = window.innerHeight;
preloadHeight = viewportHeight;
lastRenderedRow = -1;
}
connectedCallback() {
this.loadMore();
window.addEventListener("scroll", this.debounceScroll.bind(this));
}
debounceScroll() {
clearTimeout(debounceTimeoutId);
debounceTimeoutId = setTimeout(() => {
this.loadMore();
}, 250);
}
loadMore() {
if (
lastRenderedRow < rows.length &&
viewportHeight + window.scrollY > this.offsetHeight - preloadHeight
) {
this.render();
}
}
render() {
this.className = "gallery";
if (!rows || rows.length < 1) return;
const pixelsToRender = viewportHeight + window.scrollY + preloadHeight;
const i = lastRenderedRow + 1;
if (i >= rows.length) return;
if (this.offsetHeight >= pixelsToRender) {
return;
}
const r = rows[i];
// Create an instance of one of the netflix-*-row components.
const el = document.createElement(`netflix-${r.type}-row`);
// ...Set attributes on el
this.appendChild(el);
lastRenderedRow = i;
window.requestAnimationFrame(this.render.bind(this));
}
}
export default GalleryComponent;
Now let’s take a look at one of the rows.
scripts/components/HeaderRow.js
This is the header row, which shows a single large image with a couple CTA’s (removed for brevity).
class HeaderRowComponent extends HTMLElement {
constructor() {
super();
this.buttons = [];
this.hideMetadata = false;
}
connectedCallback() {
this.render();
}
render() {
this.className = "row-billboard";
this.innerHTML = `
<img class="billboard-background" src="${this.dataset.background}" />
<div class="billboard-metadata ${this.hideMetadata ? "hidden" : ""}">
<img class="billboard-metadata-logo" src="${this.dataset.logo}" />
${
this.dataset.synopsis
? `<p class="billboard-metadata-synopsis">${this.dataset.synopsis}</p>`
: ""
}
</div>
`;
}
}
export default HeaderRowComponent;
Other components use the same approach.
By using native web components, you can create a lightweight, React-like architecture without relying on external libraries or build tools. This approach demonstrates the power and flexibility of modern web standards for building dynamic, component-based applications.