This article is part of series on adding React to an existing server side rendered application.

We’ve already seen how easy it is to add React components within an existing server-side rendered page. The biggest challenge is that React is driven by state changes rather than more traditional method calls, so forcing React updates from outside React is tricky. We can’t just say something like component.changeSelectedUser(). Similarly, we can’t access a component’s internal state changes from an outside component, so invoking a change in a plain old JavaScript component can really only be done by providing callbacks to the React component.

There’s Got To Be a Better Way

While not as clean as I’d like, it is possible to set up two-way communication between independently rendered React components and between React and non-React JavaScript components. To do this, we make use of window events.

Handling Updates Within a React Component Originating From JavaScript Components

From an external component, we can publish events like this

window.dispatchEvent(new CustomEvent("resourcesChanged", {
  detail: {
    resourceIds: [1, 2, 3, 4],
  }
}));

Within the React component, we listen for the “resourcesChanged” event

function handleResourcesChanged(event: Event): void {
    onResourcesChangedExternally((event as CustomEvent).detail.resourceIds);
}

useEventListener("resourcesChanged", handleResourcesChanged);

Here’s the useEventHandler hook (which I found online at some point – I’ll update with attribution if I can find the original source)

import { useEffect, useRef } from "react";

export function useEventListener(eventName: string, handler: (event: Event) => void, element = window): void {
  const savedHandler = useRef<(e: Event) => void>();

  useEffect(() => {
    savedHandler.current = handler;
  }, [handler]);

  useEffect(() => {
    const isSupported = element && element.addEventListener;
    if (!isSupported) {
      return;
    }

    const eventListener = (event: Event): void => {
      if (savedHandler.current) {
        savedHandler.current(event);
      }
    };

    element.addEventListener(eventName, eventListener);

    return () => {
      element.removeEventListener(eventName, eventListener);
    };
  }, [eventName, element]);
}

Handling Updates Within a JavaScript Component Originating From React Components

Well, this is really just the inverse of the previous approach. React publishes events and the components listen to events.

Within the React, we can publish an event like this

window.dispatchEvent(new CustomEvent("resourcesChanged", {
  detail: {
    resourceIds: [1, 2, 3, 4],
  }
}));

Within the JavaScript code, we can handle the event like this

function onResourcesChanges(event){
    onResourcesChangedByReact(event.detail.resourceIds);
}

window.addEventListener("resourcesChanged", onResourcesChanges);

Other Approaches

There are other approaches that I’ve seen work. Some folks put a redux store or some other event processor on the global JavaScript scope. This works just fine, it is just much more effort than I wanted to take on right now. If we were handling hundreds of different events between components, then I’d put something more robust in place, but for a handful of events it’s just overkill.