A tiny web-standards based utility for event-driven web apps.
Leverages @standard-schema/spec to safely dispatch and listen to custom events in the browser OR the server by using the EventTarget interface.
- ๐ก๏ธ Zero dependencies for a lean footprint
- โ Type-safe event dispatching and listening
- ๐ Runtime validation powered by
@standard-schema/spec - ๐งช Minimal, browser-only API
- ๐ชถ Lightweight and framework-agnostic
npm install @forge42/web-events
# or
pnpm add @forge42/web-eventsThis works both on the client and server side, as it uses the EventTarget interface.
import { registerEvent } from "@forge42/web-events"
import { z } from "zod"
// Define your schema
const UserLoggedIn = z.object({
userId: z.string(),
timestamp: z.number(),
})
// Register event
const [dispatch, listener] = registerEvent("user:logged-in", UserLoggedIn)
// Listen for event
const unsubscribe = listener((data) => {
console.log("User logged in:", data.userId)
})
// Dispatch event
dispatch({
userId: "abc123",
timestamp: Date.now(),
})The registerEvent function returns a tuple containing the dispatch function and the listener function. The listener can be used to subscribe to the event, and it returns an unsubscribe function to stop listening.
const [dispatch, listener] = registerEvent("user:logged-in", UserLoggedIn)Once the event is registered, you can dispatch it with the expected data structure, and it will be validated against the schema.
// This will match the schema provided to the registerEvent function
dispatch({
userId: "abc123",
timestamp: Date.now(),
})If you want to listen to the incoming events in your application, you can use the listener function. It accepts a callback that will be invoked with the validated data whenever the event is dispatched.
const unsubscribe = listener((data) => {
console.log("User logged in:", data.userId)
})
// Optionally you can unsubscribe later
unsubscribe()You can also pass in a third optional parameter to the registerEvent function to specify the event init options, such as bubbles, cancelable, or composed. This allows you to control the behavior of the event in the DOM.
It also takes in an optional property emitter which can be used to specify the EventTarget that will emit the event. This is useful if you want to use a custom event target instead of the default one.
const [dispatch, listener] = registerEvent("user:logged-in", UserLoggedIn, {
bubbles: true,
composed: true,
emitter: document // or any other EventTarget
})You can also use @forge42/web-events in a React application. It comes with a React specific API that allows you to register and listen to events in a more React-friendly way.
import { registerReactEvent } from "@forge42/web-events/react"
import { z } from "zod"
// Define your schema
const UserLoggedIn = z.object({
userId: z.string(),
timestamp: z.number(),
})
// Register event
const [dispatchUserLogin, useUserLoggedInEvent] = registerReactEvent("user:logged-in", UserLoggedIn)
// Use the event in a React component
export default function UserComponent() {
const userLoggedIn = useUserLoggedInEvent()
const handleLogin = () => dispatchUserLogin({
userId: "abc123",
timestamp: Date.now(),
})
return (
<div>
<button onClick={handleLogin}>Log In</button>
{userLoggedIn && <p>User logged in: {userLoggedIn.userId}</p>}
</div>
)
}The registerReactEvent function returns a tuple containing the dispatch function and a custom React hook (useEventListener) that you can use to access the event data in your components. The hook accepts
an object with an onEvent callback that will be called whenever the event is received.
const [dispatch, useEventListener] = registerReactEvent("user:logged-in", UserLoggedIn)
// Use the event in a React component
export default function UserComponent() {
const userLoggedIn = useEventListener({
onEvent: (data) => {
console.log("User logged in:", data.userId)
},
})
const handleLogin = () => {
dispatch({
userId: "abc123",
timestamp: Date.now(),
})
}
return (
<div>
<button onClick={handleLogin}>Log In</button>
{userLoggedIn && <p>User logged in: {userLoggedIn.userId}</p>}
</div>
)
}The useEvent hook is a custom React hook that allows you to listen to events in a more declarative way. It accepts an initialized registerEvent instance and returns the data.
import { useEvent } from "@forge42/web-events/react"
const messageSentEvent = registerEvent("message:sent", z.object({
message: z.string(),
count: z.number().optional(),
date: z.date().optional(),
}));
export default function MessageComponent() {
const messageSent = useEvent(messageSentEvent, {
onEvent: (data) => {
console.log("Message sent:", data);
},
});
return (
<div>
{messageSent ? (
<p>Message: {messageSent.message}, Count: {messageSent.count}, Date: {messageSent.date?.toISOString()}</p>
) : (
<p>No message sent yet.</p>
)}
</div>
);
}
The library accepts any schema compatible with @standard-schema/spec, such as Zod, Yup, or Joi. This allows you to define the structure of your event data and ensures that only valid data is dispatched.
import { z } from "zod"
const UserLoggedIn = z.object({
userId: z.string(),
timestamp: z.number(),
})You can easily integrate @forge42/web-events with popular frameworks like React, Vue, or Svelte. Here's an example for React:
import React, { useEffect } from "react"
import { registerEvent } from "@forge42/web-events"
const UserLoggedIn = z.object({
userId: z.string(),
timestamp: z.number(),
})
// Do not initialize the event inside the component to avoid re-registering on every render
const [dispatch, listener] = registerEvent("user:logged-in", UserLoggedIn)
const UserComponent = () => {
useEffect(() => {
const unsubscribe = listener((data) => {
console.log("User logged in:", data.userId)
})
return () => {
unsubscribe() // Cleanup on unmount
}
}, [])
const handleLogin = () => {
dispatch({
userId: "abc123",
timestamp: Date.now(),
})
}
return <button onClick={handleLogin}>Log In</button>
}
export default UserComponentThis utility uses your schema to infer:
dispatch(input) expects data that matches the input schema
listener(callback) receives validated and parsed data
Use it to:
Broadcast form submissions
Track user interactions across decoupled modules
Replace fragile string-based pub/sub logic with type-safe guarantees
MIT License