Retool Custom Components - React Typescript Library

I am curious if there is a repo available for us to submit pull requests against or bug reports for the Retool library.

I have been developing with it a little and noticed that Retool.useStateObject is not generic. Is there a reason why?

useStateEnumeration is generic and correctly types the value.

1 Like

Question also about Expanding Your Library...

If the one "library" we create, exports multiple components...

Is it possible for two components from the same library to talk to each other?

Example

Component A Some sort of visualization component
Component B Buttons and controls for Component A

Would there be a way to click buttons in Component B and have Component A react?

1 Like

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 };
}
2 Likes