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!
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!
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