Retool Custom Components - React Typescript Library

I actually solved this already! :slight_smile:

I came up with the idea to use localStorage as a pretend message bus. It already has events, so it wasn't to hard to implement. At the end is the code for the LocalStorageEventManager but first is how it is used.

index.ts

This is our main component library file. We export two components, the listener Component A and the emitter, Component B. They are as follows...

import React from "react";
import { Retool } from "@tryretool/custom-component-support";
import { LocalStorageEventManager } from "./LocalStorageEventManager";

// Defined a Payload
type ControlMessage = {
  action: "fit" | "expandAll" | "collapseAll";
};

// Define Events with Payloads
type Events = {
  CONTROL_CLICKED: ControlMessage;
};

// Create an instance with a namespace on localStorage
const LSEM = LocalStorageEventManager<Events>({ namespace: "chartControls" });

/**
 * Component A
 */
export const Chart: React.FC = () => {
  // Get the `useEffect` hook for an event
  // The hook is strongly typed for the listener
  const onControlClicked = LSEM.getListenerHook("CONTROL_CLICKED");

  // Register the hook and listen for the event
  // data will be `ControlMessage`
  onControlClicked((data) => {
    console.log("control clicked", data);
    // data will be { action: "fit" | "expandAll" | "collapseAll" }
  });

  return (  /* your component */  );
};

/**
 * Component B
 */
export const ChartControls: React.FC = () => {
  // Get the emitter for an event
  const emit = LSEM.getEmitter("CONTROL_CLICKED");

  // emit is strongly typed emit(data: ControlMessage) => void
  // emit({ action: "nope" }) will produce a typescript error

  return (
    <div>
      <button onClick={() => emit({ action: "fit" })}>Fit To Screen</button>
      <button onClick={() => emit({ action: "expandAll" })}>Expand All</button>
      <button onClick={() => emit({ action: "collapseAll" })}>Collapse All</button>
    </div>
  );
};

This solved my problem of having two separate components communicate with each other! :tada:

LocalStorageEventManager

import { useEffect } from "react";

import type { Retool } from "@tryretool/custom-component-support";

export function LocalStorageEventManager<
  T extends Record<string, Retool.SerializableType>
>(config?: { namespace: string }) {
  const namespace = config?.namespace ?? `LSEM_${new Date().getTime()}`;
  const context = {
    loaded: localStorage.getItem(namespace),
    parsed: {}
  };

  if (!context.loaded) {
    localStorage.setItem(namespace, "{}");
    context.parsed = {};
  } else {
    context.parsed = JSON.parse(context.loaded);
  }

  const getEmitter = (emitter: keyof T) => {
    const emit = (data: T[keyof T]): void => {
      try {
        const context = {
          loaded: localStorage.getItem(namespace),
          parsed: {}
        };

        if (!context.loaded) {
          localStorage.setItem(namespace, "{}");
          context.parsed = {};
        } else {
          context.parsed = JSON.parse(context.loaded);
        }

        localStorage.setItem(
          namespace,
          JSON.stringify({
            ...context.parsed,
            [emitter]: data
          })
        );
      } catch (error) {
        console.error("Failed to emit localStorage event:", error);
      }
    };

    return emit;
  };

  const getListenerHook = (eventName: keyof T) => {
    const listen = (onMessage: (data: T[keyof T]) => void) => {
      useEffect(() => {
        const handleStorageEvent = (event: StorageEvent) => {
          if (event.key === namespace && event.newValue) {
            try {
              const parsedData = JSON.parse(event.newValue) as Record<
                keyof T,
                T[keyof T]
              >;
              const eventData = parsedData[eventName];

              if (eventData !== undefined) {
                onMessage(eventData);

                // Use object destructuring to exclude eventName
                const { [eventName]: _, ...updatedData } = parsedData;
                localStorage.setItem(namespace, JSON.stringify(updatedData));
              }
            } catch (error) {
              console.error("Failed to parse localStorage message:", error);
            }
          }
        };

        window.addEventListener("storage", handleStorageEvent);

        return () => {
          window.removeEventListener("storage", handleStorageEvent);
        };
      }, [namespace, eventName, onMessage]);
    };

    return listen;
  };

  return { getEmitter, getListenerHook };
}
3 Likes