Typesafe Custom Events

← Back to articlesover 1 year ago | by Håkon Underbakke

Here’s a useful abstraction on top of custom events that let you more easily set up typesafe channels to communicate between.

typesafe-custom-events.ts

type UnsubscribeFunction = () => void;
const prefix = "global-prefix"; // To prevent event-name collision
const generateId = () => {
  /* should return a unique id */
};

export class CustomEventChannel<T> {
  name: string;
  id: string;
  send: (args: T) => void;
  subscribe: (onEvent: (event: T) => any) => UnsubscribeFunction;

  constructor(name?: string) {
    this.id = generateId();
    this.name = name ?? `${prefix}-${this.id}`;
    this.send = (args: T) => {
      if (args === undefined) return;
      document.dispatchEvent(
        new CustomEvent(this.name, { detail: args })
      );
    };
    this.subscribe = (
      onEvent: (event: T) => void
    ): UnsubscribeFunction => {
      const listener = (e: Event) => {
        const event = e as Event & {
          detail?: T;
        };
        if (event.detail !== undefined) {
          onEvent(event.detail);
        }
      };
      document.addEventListener(this.name, listener);
      return () => {
        document.removeEventListener(this.name, listener);
      };
    };
  }
}

// use
type MyEventFormat = {
  message: string;
  isCool: boolean;
};
const channel = new CustomEventChannel<MyEventFormat>(
  "coolChannel"
);

channel.subscribe((event) => {
  console.log("New message: ", event.message, event.isCool);
});

channel.send({
  message: "Foobar",
  isCool: true,
});
// "New message: Foobar, true"

This can be used in React like a hook:

App.tsx

const useChannel = <T extends unknown>(
  channel: CustomEventChannel<T>,
  onEvent: (event: T) => void
) => {
  React.useEffect(() => {
    const unsubscribe = channel.subscribe(onEvent);
    return unsubscribe;
  }, []);
};

// use
const myChannel = new CustomEventChannel<{
  content: React.ReactNode;
}>("my-channel");

const App = () => {
  const [events, setEvents] = useState<T[]>([]);
  const events = useChannel(channel, (event) =>
    setEvents([...events, event])
  );
  // ...
};

I’ve also published this as an NPM package, typesafe-custom-events, which you can check out here.

Edit this page @ Github 🧑‍💻