Access scope methods with useAui.
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 object has a property for every scope available in the current context. Crucially, useAui() does not re-render your component when scopes change: it returns a stable reference. The actual scope is only resolved when you call aui.counter().
Scope resolution
aui.counter is not the scope itself, it's an accessor. The scope resolves when you call it:
// resolves the counter scope, returns its methods
aui.counter().increment();This distinction matters. The aui object is stable across re-renders and scope changes. When a derived scope switches which item it points to, aui stays the same, but aui.counter() returns the new scope's methods. This is why you should always resolve 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 resolve during render
Because useAui() doesn't subscribe to scope changes, resolving during render gives you a snapshot that can go stale. Use useAuiState to read state during render instead.
const Counter = () => {
const aui = useAui();
// ❌ Don't resolve during render
const count = aui.counter().getState().count;
// ✅ Use useAuiState for render-time reads
const count = useAuiState((s) => s.counter.count);
// ✅ Resolve in event handlers, effects, or callbacks
const handleClick = () => aui.counter().increment();
};For the same reason, avoid storing a resolved scope in a variable during render:
// ❌ Resolves during render, can go stale
const counter = aui.counter();
const handleClick = () => counter.increment();
// ✅ Resolves at call time, always current
const handleClick = () => aui.counter().increment();Checking if a scope exists
Calling aui.counter() throws if the counter scope hasn't been provided by any AuiProvider above. To safely check, inspect the accessor's source property:
const aui = useAui();
if (aui.counter.source !== null) {
// safe to call
aui.counter().increment();
}source is null when the scope isn't available. Any other value ("root", a parent scope name) means it's safe to resolve.
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.
Since useAui() doesn't re-render on scope changes, you need to opt in explicitly. Use useAuiState to subscribe to the scope identity:
const thread = useAuiState(() => aui.thread());
useEffect(() => {
analytics.register(thread);
return () => analytics.unregister(thread);
}, [thread]);aui.thread() returns a stable methods object per scope instance. When a derived scope switches which thread it points to, useAuiState detects the new reference and re-renders, triggering the effect cleanup and re-registration.