Documents
methods
methods
Type
External
Status
Published
Created
Mar 17, 2026
Updated
Mar 17, 2026

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, tapState } from "@assistant-ui/tap";
import type { ClientOutput } from "@assistant-ui/store";

const CounterResource = resource((): ClientOutput<"counter"> => {
  const [count, setCount] = tapState(0);

  return {
    increment: () => setCount((c) => c + 1),
    decrement: () => setCount((c) => c - 1),
    reset: () => setCount(0),
  };
});

Every function you return becomes a method on the scope. There's nothing special about them — they're plain functions that can call tapState 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.