e30e75b5d4
Changesets / Create Version PR (push) Has been cancelled
Deploy Shadcn Registry / Deploy Production (push) Has been cancelled
Template Metrics / LOC + Bundle Size (push) Has been cancelled
Code Quality / Oxlint + Oxfmt (push) Has been cancelled
Code Quality / Template Sync (push) Has been cancelled
Code Quality / Build Changed Packages (push) Has been cancelled
Code Quality / Test Changed Packages (push) Has been cancelled
Deploy Expo Example / Deploy Production (push) Has been cancelled
Deploy Ink Example / Deploy Production (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.12) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Has been cancelled
167 lines
5.3 KiB
Plaintext
167 lines
5.3 KiB
Plaintext
---
|
|
title: Events
|
|
description: Emit and subscribe to typed events.
|
|
---
|
|
|
|
State tells you what things look like right now. Events tell you what just happened. Use events for side effects that respond to discrete actions: showing a toast, scrolling to a new message, firing analytics.
|
|
|
|
## Declaring events
|
|
|
|
Add an `events` field to your scope in `ScopeRegistry`. Event names follow the format `"scopeName.eventName"`:
|
|
|
|
```ts title="lib/store/counter-scope.ts"
|
|
import "@assistant-ui/store";
|
|
|
|
declare module "@assistant-ui/store" {
|
|
interface ScopeRegistry {
|
|
counter: {
|
|
methods: {
|
|
getState: () => { count: number };
|
|
increment: () => void;
|
|
};
|
|
events: {
|
|
"counter.incremented": { newCount: number };
|
|
"counter.reset": undefined;
|
|
};
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
Each event has a typed payload. Use `undefined` for events that carry no data.
|
|
|
|
## Emitting events
|
|
|
|
Call `useAssistantEmit()` inside a resource to get an emit function:
|
|
|
|
```ts title="lib/store/counter-resource.ts"
|
|
import { resource } from "@assistant-ui/tap";
|
|
import { useState, useMemo } from "react";
|
|
import { useAssistantEmit } from "@assistant-ui/store";
|
|
import type { ClientOutput } from "@assistant-ui/store";
|
|
|
|
const useCounterResource = (): ClientOutput<"counter"> => {
|
|
const [count, setCount] = useState(0);
|
|
const emit = useAssistantEmit();
|
|
|
|
const state = useMemo(() => ({ count }), [count]);
|
|
|
|
return {
|
|
getState: () => state,
|
|
increment: () => {
|
|
const newCount = count + 1;
|
|
setCount(newCount);
|
|
emit("counter.incremented", { newCount });
|
|
},
|
|
};
|
|
};
|
|
|
|
const CounterResource = resource(useCounterResource);
|
|
```
|
|
|
|
Events are delivered via microtask: they fire after the current state update settles, not mid-update. This means listeners always see consistent state.
|
|
|
|
## Subscribing to events
|
|
|
|
Use `useAuiEvent` in any component inside an `AuiProvider`:
|
|
|
|
```tsx
|
|
import { useAuiEvent } from "@assistant-ui/store";
|
|
|
|
const CounterToast = () => {
|
|
useAuiEvent("counter.incremented", ({ newCount }) => {
|
|
toast(`Count is now ${newCount}`);
|
|
});
|
|
|
|
return null;
|
|
};
|
|
```
|
|
|
|
The callback receives the typed payload. It's stable internally (via `useEffectEvent`), so you don't need to memoize it.
|
|
|
|
## Event scoping
|
|
|
|
When you pass a string like `"counter.incremented"` to `useAuiEvent`, it listens for events from the `counter` scope **in your current context**. This is scope filtering: the listener only fires when the event comes from the specific scope instance you're inside.
|
|
|
|
This matters when there are multiple instances of the same scope. Consider a list of counters, each with its own `counter` scope:
|
|
|
|
```
|
|
App
|
|
└ CounterItem (counter scope, id: "a")
|
|
└ CounterItem (counter scope, id: "b")
|
|
└ CounterItem (counter scope, id: "c")
|
|
```
|
|
|
|
A `useAuiEvent("counter.incremented", ...)` inside `CounterItem "a"` only fires for that counter, not for "b" or "c". Each listener is scoped to its own instance.
|
|
|
|
### Listening to child events
|
|
|
|
If you try to listen to an event from a scope that isn't available in your component's context, Store throws an error. For example, if your component sits above the `counter` scope, `useAuiEvent("counter.incremented", ...)` will throw because there is no `counter` scope to filter against.
|
|
|
|
To listen to events from child scopes, set `scope` to your own scope (the parent):
|
|
|
|
```tsx
|
|
// inside a component that has counterList but not counter
|
|
useAuiEvent(
|
|
{ scope: "counterList", event: "counter.incremented" },
|
|
({ newCount }) => {
|
|
console.log("some counter changed:", newCount);
|
|
},
|
|
);
|
|
```
|
|
|
|
This fires for `counter.incremented` events from any counter instance below the `counterList` scope. The `scope` field tells Store which level to filter at: any event emitted by a descendant of that scope instance will match.
|
|
|
|
### Listening to sibling events
|
|
|
|
There's no direct way to listen to events from a sibling scope. Instead, listen at a mutual parent (or `"*"`) and filter by metadata in the event payload:
|
|
|
|
```tsx
|
|
// inside counter "a", listening for increments from any sibling
|
|
useAuiEvent(
|
|
{ scope: "counterList", event: "counter.incremented" },
|
|
({ counterId, newCount }) => {
|
|
if (counterId === "b") {
|
|
console.log("counter b incremented:", newCount);
|
|
}
|
|
},
|
|
);
|
|
```
|
|
|
|
Include whatever metadata you need for filtering in the event payload; Store doesn't provide sibling identity automatically.
|
|
|
|
### Wildcard scope
|
|
|
|
Use `"*"` as the scope to listen to an event from any instance, regardless of your position in the tree:
|
|
|
|
```tsx
|
|
useAuiEvent(
|
|
{ scope: "*", event: "counter.incremented" },
|
|
({ newCount }) => {
|
|
console.log("any counter incremented:", newCount);
|
|
},
|
|
);
|
|
```
|
|
|
|
### Wildcard event
|
|
|
|
Use `"*"` as the event to listen to every event. The payload is wrapped with the event name:
|
|
|
|
```tsx
|
|
useAuiEvent("*", ({ event, payload }) => {
|
|
console.log(event, payload);
|
|
});
|
|
```
|
|
|
|
This is useful for debugging and logging.
|
|
|
|
## Choosing a scoping pattern
|
|
|
|
| You want to... | Selector |
|
|
| --- | --- |
|
|
| Listen to events from your own scope | `"scope.event"` |
|
|
| Listen to events from any child below you | `{ scope: "yourScope", event: "child.event" }` |
|
|
| Listen to events from a sibling | `{ scope: "mutualParent", event: "sibling.event" }` + filter by payload |
|
|
| Listen to events from anywhere | `{ scope: "*", event: "scope.event" }` |
|
|
| Listen to all events (debugging) | `"*"` |
|