Methods

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:

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:

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:

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, so 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

Info

This is an advanced pattern. assistant-ui itself uses it in only a few places.

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.