React UI
Lifecycle and state
Handle idle, streaming, error, stop, reset, regenerate, and loading states in React UI surfaces.
@anvia/react-ui reflects the @anvia/react controller state. Your app decides where to place
status, errors, stop buttons, retry controls, and reset actions.
Chat states
useChat(...) exposes three status values:
| Status | Meaning | Common UI |
|---|---|---|
idle |
No request is active. | Enable send and regenerate. |
streaming |
The route is sending events. | Show stop, disable submit, auto-scroll. |
error |
The latest request failed. | Show error and let the user retry. |
const chat = useChat({ endpoint: "http://localhost:8787/api/chat" });
return (
<ChatProvider controller={chat}>
<Thread.Status>
{(status) => (status === "streaming" ? "Thinking..." : "Ready")}
</Thread.Status>
<Thread.Error />
<Composer.Root>
<Composer.Input />
<Composer.Stop>Stop</Composer.Stop>
<Composer.Submit>Send</Composer.Submit>
</Composer.Root>
</ChatProvider>
);
Stop streaming
Composer.Stop calls chat.stop() and disables itself when nothing can be stopped.
<Composer.Stop className="composer-button">Stop</Composer.Stop>
Use the controller directly when the stop action lives outside the composer:
<button type="button" disabled={chat.status !== "streaming"} onClick={chat.stop}>
Stop response
</button>
Reset conversation
Use reset() for a new chat:
<button type="button" onClick={() => chat.reset()}>
New chat
</button>
Pass messages to reset into a known history:
chat.reset(savedMessages);
Regenerate
Message.Regenerate is enabled for the latest assistant message while the chat is idle.
<Thread.Messages>
{(message) => (
<Message.Root>
<Message.Content>
<Message.Parts />
</Message.Content>
{message.role === "assistant" ? (
<Message.Actions>
<Message.Regenerate>Try again</Message.Regenerate>
</Message.Actions>
) : null}
</Message.Root>
)}
</Thread.Messages>
Use chat.regenerate() directly for a toolbar action:
<button type="button" disabled={chat.status !== "idle"} onClick={() => void chat.regenerate()}>
Regenerate latest
</button>
Error state
Thread.Error renders the controller error. Use a child function when the product needs a custom
message or retry button.
<Thread.Error className="thread-error">
{(error) => (
<div role="alert">
<strong>Message failed</strong>
<p>{error instanceof Error ? error.message : "The stream failed."}</p>
<button type="button" onClick={() => void chat.regenerate()}>
Retry
</button>
</div>
)}
</Thread.Error>
Loading indicator
Use Thread.Loading for a loading area inside the viewport:
<Thread.Loading className="loading-row">Generating...</Thread.Loading>
Or branch from controller state:
{chat.status === "streaming" ? <div className="typing">Assistant is writing...</div> : null}
Completion states
Completion controllers expose the same lifecycle concepts:
const completion = useCompletion({ endpoint: "http://localhost:8787/api/completion" });
return (
<CompletionProvider controller={completion}>
<Completion.Root>
<Completion.Output />
<Completion.Form>
<Completion.Input />
<Completion.Stop>Stop</Completion.Stop>
<Completion.Submit>Generate</Completion.Submit>
</Completion.Form>
</Completion.Root>
</CompletionProvider>
);
For lifecycle tests, keep assertions around visible UI state: submit disabled while streaming, stop enabled while streaming, error visible after a failed route, and reset clearing message state.
