tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

ExternalComponentWrapper.jsx (4188B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import React from "react";
      6 import { useSelector } from "react-redux";
      7 
      8 /**
      9 * A React component that dynamically loads and embeds external custom elements
     10 * into the newtab page.
     11 *
     12 * This component serves as a bridge between React's declarative rendering and
     13 * browser-native custom elements that are registered and managed outside of
     14 * React's control. It:
     15 *
     16 * 1. Looks up the component configuration by type from the ExternalComponents
     17 *    registry
     18 * 2. Dynamically imports the component's script module (which registers the
     19 *    custom element)
     20 * 3. Creates an instance of the custom element using imperative DOM APIs
     21 * 4. Appends it to a React-managed container div
     22 * 5. Cleans up the custom element on unmount
     23 *
     24 * This approach is necessary because:
     25 * - Custom elements have their own lifecycle separate from React
     26 * - They need to be created imperatively (document.createElement) rather than
     27 *   declaratively (JSX)
     28 * - React shouldn't try to diff/reconcile their internal DOM, as they manage
     29 *   their own shadow DOM
     30 * - We need manual cleanup to prevent memory leaks when the component unmounts
     31 *
     32 * @param {object} props
     33 * @param {string} props.type - The component type to load (e.g., "SEARCH")
     34 * @param {string} props.className - CSS class name(s) to apply to the wrapper div
     35 * @param {Function} props.importModule - Function to import modules (for testing)
     36 */
     37 function ExternalComponentWrapper({
     38  type,
     39  className,
     40  // importFunction is declared as an arrow function here purely so that we can
     41  // override it for testing.
     42  // eslint-disable-next-line no-unsanitized/method
     43  importModule = url => import(/* webpackIgnore: true */ url),
     44 }) {
     45  const containerRef = React.useRef(null);
     46  const customElementRef = React.useRef(null);
     47  const l10nLinksRef = React.useRef([]);
     48  const [error, setError] = React.useState(null);
     49  const { components } = useSelector(state => state.ExternalComponents);
     50 
     51  React.useEffect(() => {
     52    const container = containerRef.current;
     53 
     54    const loadComponent = async () => {
     55      try {
     56        const config = components.find(c => c.type === type);
     57 
     58        if (!config) {
     59          console.warn(
     60            `No external component configuration found for type: ${type}`
     61          );
     62          return;
     63        }
     64 
     65        await importModule(config.componentURL);
     66 
     67        l10nLinksRef.current = [];
     68        for (let l10nURL of config.l10nURLs) {
     69          const l10nEl = document.createElement("link");
     70          l10nEl.rel = "localization";
     71          l10nEl.href = l10nURL;
     72          document.head.appendChild(l10nEl);
     73          l10nLinksRef.current.push(l10nEl);
     74        }
     75 
     76        if (containerRef.current && !customElementRef.current) {
     77          const element = document.createElement(config.tagName);
     78 
     79          if (config.attributes) {
     80            for (const [key, value] of Object.entries(config.attributes)) {
     81              element.setAttribute(key, value);
     82            }
     83          }
     84 
     85          if (config.cssVariables) {
     86            for (const [variable, style] of Object.entries(
     87              config.cssVariables
     88            )) {
     89              element.style.setProperty(variable, style);
     90            }
     91          }
     92 
     93          customElementRef.current = element;
     94          containerRef.current.appendChild(element);
     95        }
     96      } catch (err) {
     97        console.error(
     98          `Failed to load external component for type ${type}:`,
     99          err
    100        );
    101        setError(err);
    102      }
    103    };
    104 
    105    loadComponent();
    106 
    107    return () => {
    108      if (customElementRef.current && container) {
    109        container.removeChild(customElementRef.current);
    110        customElementRef.current = null;
    111      }
    112 
    113      for (const link of l10nLinksRef.current) {
    114        link.remove();
    115      }
    116      l10nLinksRef.current = [];
    117    };
    118  }, [type, components, importModule]);
    119 
    120  if (error) {
    121    return null;
    122  }
    123 
    124  return <div ref={containerRef} className={className} />;
    125 }
    126 
    127 export { ExternalComponentWrapper };