React, but with Web Components

Posted by Dustin Boston on in .

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.