Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:40:13 +08:00

152 lines
4.7 KiB
Plaintext

---
title: Methods
description: 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`:
```ts title="lib/store/counter-scope.ts"
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:
```ts title="lib/store/counter-resource.ts"
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:
```tsx
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:
```tsx
// 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:
```tsx
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.
```tsx
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:
```tsx
// ❌ 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:
```tsx
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
<Callout type="info">
This is an advanced pattern. In the entire assistant-ui codebase, there are only two use cases for this.
</Callout>
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:
```tsx
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.