Methods are the imperative API of a scope. They're the functions your resource returns: increment, send, delete, or anything else. You access them through useAui().
Defining methods#
First, register the method signatures in ScopeRegistry:
import "@assistant-ui/store";
declare module "@assistant-ui/store" {
interface ScopeRegistry {
counter: {
methods: {
increment: () => void;
decrement: () => void;
reset: () => void;
};
};
}
}
Then create a resource that implements them. The return type ClientOutput<"counter"> ties the resource to the scope: TypeScript will error if the returned methods don't match the registry:
import { resource } from "@assistant-ui/tap";
import { useState } from "react";
import type { ClientOutput } from "@assistant-ui/store";
const useCounterResource = (): ClientOutput<"counter"> => {
const [count, setCount] = useState(0);
return {
increment: () => setCount((c) => c + 1),
decrement: () => setCount((c) => c - 1),
reset: () => setCount(0),
};
};
const CounterResource = resource(useCounterResource);
Every function you return becomes a method on the scope. There's nothing special about them: they're plain functions that can call useState setters, trigger side effects, or do anything else.
useAui#
Call useAui() with no arguments inside any AuiProvider to get the current store:
const aui = useAui();
The returned client has an accessor for every scope available in the current context. The client is immutable: state updates never change its identity, while a structural change (a scope resolving to a different instance) produces a new client and re-renders consumers through React.
Scope accessors#
aui.counter is the scope accessor: it exposes the scope's methods plus source/query/name metadata. Its identity is stable per scope binding and changes when the binding changes:
aui.counter.increment();
When a derived scope switches which item it points to, the component re-renders with a new client and a new accessor. Access scopes at the point of use:
const MessageActions = () => {
const aui = useAui();
return (
<button
onClick={() => {
// resolves at click time, always gets the current scope
aui.message.reload();
aui.thread.cancelRun();
}}
/>
);
};
Don't read state during render#
aui.counter.getState() returns a snapshot without subscribing, so render output built from it goes stale. Use useAuiState to read state during render instead.
const Counter = () => {
const aui = useAui();
// ❌ Don't read state during render
const count = aui.counter.getState().count;
// ✅ Use useAuiState for render-time reads
const count = useAuiState((s) => s.counter.count);
// ✅ Read in event handlers, effects, or callbacks
const handleClick = () => aui.counter.increment();
};
Checking if a scope exists#
Accessing aui.counter never throws, and the accessor is always truthy — if (aui.counter) does not tell you anything. When the scope hasn't been provided by any AuiProvider above, the accessor still answers source (null), query, and name; calling it or reading any other property throws. Read the scope through aui.optional, which resolves to the same accessor when available and undefined when not:
const aui = useAui();
aui.optional.counter?.increment();
This mirrors s.optional on the state side. The accessor metadata keeps working, and aui.optional.counter is undefined whenever the scope is not available, whether its accessor reports source: null or the scope is entirely absent from a hand-built parent chain.
Subscribing to scope identity#
This is an advanced pattern. In the entire assistant-ui codebase, there are only two use cases for this.Sometimes you need to know when the scope itself changes, for example to register/unregister with an external system when a derived scope switches to a different item.
Use useAuiState to subscribe to the scope identity:
const thread = useAuiState(() => aui.thread);
useEffect(() => {
analytics.register(thread);
return () => analytics.unregister(thread);
}, [thread]);
aui.thread is a stable accessor per scope binding. When a derived scope switches which thread it points to, useAuiState detects the new accessor and re-renders, triggering the effect cleanup and re-registration.