Typed custom event emitters with runtime validation via Standard Schema. Framework-agnostic core with optional React bindings. Works with any compatible validation library — no adapter needed.
| Library | Minimum Version |
|---|---|
| Zod | 3.24.0 |
| Valibot | 1.0.0 |
| ArkType | 2.0.0 |
| Effect Schema | 3.13.0 |
| Yup | 1.7.0 |
| Joi | 18.0.0 |
| Typia | 9.2.0 |
| VineJS | 4.0.0 |
Any library that implements the Standard Schema spec works out of the box.
pnpm add use-custom-event| Import Path | Description | Requires React |
|---|---|---|
use-custom-event |
Core event emitter (DOM CustomEvent) | No |
use-custom-event/react |
Core + useEventListener hook |
Yes |
use-custom-event/broadcast |
BroadcastChannel event emitter | No |
use-custom-event/broadcast/react |
Broadcast + useEventListener hook |
Yes |
import { z } from "zod";
import { createEventEmitter } from "use-custom-event";
const { emit, subscribe } = createEventEmitter("my-event", z.object({ name: z.string() }));
const unsubscribe = subscribe((data) => {
console.log(data.name); // strictly typed
});
unsubscribe();import { z } from "zod";
import { createEventEmitter } from "use-custom-event/react";
const { emit, subscribe, useEventListener } = createEventEmitter(
"my-event",
z.object({ name: z.string() }),
);
function App() {
useEventListener(
useCallback((data) => {
console.log(data.name);
}, []),
);
return <button onClick={() => emit({ name: "hello" })}>Trigger</button>;
}import * as v from "valibot";
import { createEventEmitter } from "use-custom-event";
const { emit, subscribe } = createEventEmitter("my-event", v.object({ name: v.string() }));import { type } from "arktype";
import { createEventEmitter } from "use-custom-event";
const { emit, subscribe } = createEventEmitter("my-event", type({ name: "string" }));import { Schema } from "effect";
import { createEventEmitter } from "use-custom-event";
const { emit, subscribe } = createEventEmitter("my-event", Schema.Struct({ name: Schema.String }));import * as yup from "yup";
import { createEventEmitter } from "use-custom-event";
const { emit, subscribe } = createEventEmitter(
"my-event",
yup.object({ name: yup.string().required() }),
);Broadcast Channel enables communication between tabs/windows of the same origin. Works with any compatible validation library.
import { z } from "zod";
import { createBroadcastChannelEventEmitter } from "use-custom-event/broadcast";
const channel = new BroadcastChannel("my-channel");
const { emit, subscribe } = createBroadcastChannelEventEmitter(
channel,
z.object({ name: z.string() }),
);
const unsubscribe = subscribe((data) => {
console.log(data.name);
});
unsubscribe();import { z } from "zod";
import { createBroadcastChannelEventEmitter } from "use-custom-event/broadcast/react";
const channel = new BroadcastChannel("my-channel");
const { emit, useEventListener } = createBroadcastChannelEventEmitter(
channel,
z.object({ name: z.string() }),
);
function App() {
useEventListener(
useCallback((data) => {
console.log(data.name);
}, []),
);
return <button onClick={() => emit({ name: "hello" })}>Trigger</button>;
}v4 decouples React bindings from the core modules. The root entry point (use-custom-event) and broadcast entry point (use-custom-event/broadcast) no longer depend on React or export useEventListener. Import from the /react subpath instead:
- import { createEventEmitter } from "use-custom-event";
+ import { createEventEmitter } from "use-custom-event/react";- import { createBroadcastChannelEventEmitter } from "use-custom-event/broadcast";
+ import { createBroadcastChannelEventEmitter } from "use-custom-event/broadcast/react";If you only use emit and subscribe (no hooks), your imports stay the same — no changes needed.
v3 replaces the zod peer dependency with Standard Schema support. Your existing zod schemas work unchanged if you upgrade zod to 3.24.0+. You can also switch to any other compatible library.
- import { z } from "zod"; // zod < 3.24
+ import { z } from "zod"; // zod >= 3.24 (no code changes needed)Or switch validation libraries entirely:
- import { z } from "zod";
+ import * as v from "valibot";
const emitter = createEventEmitter(
"my-event",
- z.object({ name: z.string() }),
+ v.object({ name: v.string() }),
);MIT