Add an AI chat to a React app
Install @aparte/react, render <AparteChat>, stream a model with useAparteClient, and put your own UI in its slots — in a React app, Next.js included.
Install
Section titled “Install”npm install @aparte/react @aparte/core
npm install @aparte/provider-scenario # a scripted model, to stream with no key@aparte/core, react and react-dom are peer dependencies: React ^18.0.0 || ^19.0.0.
Render the chat
Section titled “Render the chat”import { AparteChat, useAparteChat } from '@aparte/react';
import '@aparte/core/styles.css';
export function Chat() {
const chat = useAparteChat();
return (
<AparteChat
ref={chat.ref}
messages={chat.messages}
onMessagesChange={chat.setMessages}
emptyState={<p>Ask me anything…</p>}
centerWhenEmpty
/>
);
}useAparteChat holds the messages and the component’s ref: hand both to <AparteChat>. The
person’s message is added when they send, so do not add it yourself; onMessageSent fires after,
for a side effect. In the Next App Router, put 'use client' at the top of the file that calls
useAparteChat and registers the provider: hooks run in a client component. Next still renders it
to HTML on the server, where @aparte/core resolves to its entry without elements
(On the server).
Stream a model
Section titled “Stream a model”import { aparteGlobalConfig } from '@aparte/core';
import { createScenarioProvider } from '@aparte/provider-scenario';
import { showcase } from '@aparte/provider-scenario/showcase';
import { AparteChat, useAparteChat, useAparteClient } from '@aparte/react';
aparteGlobalConfig.registerAIProvider(createScenarioProvider({ scenarios: showcase }));
export function Chat() {
const chat = useAparteChat();
useAparteClient();
return <AparteChat ref={chat.ref} messages={chat.messages} onMessagesChange={chat.setMessages} />;
}useAparteClient starts a client when the component mounts, stops it when it unmounts, and returns
{ client, abort }. It takes the AparteClientOptions and reads them once, on the first render: a
function option keeps the values it closed over then, so read live state from a ref inside it, and
remount the component with a new key to change the options. For a real model, register its
provider, name the model and give the key — aparteGlobalConfig.setModelConfig({ defaultProvider: 'openrouter', defaultModel: 'openai/gpt-4o-mini' }) and useAparteClient({ keyResolver })
(Connect a model). A config prop gives one <AparteChat> providers and a
transport of its own; give its client the same one, useAparteClient({ config }). With the client
running, turn on retry and edit with aparteGlobalConfig.setBubbleActions({ retry: true, edit: true })
(Buttons).
Put your own UI in it
Section titled “Put your own UI in it”<AparteChat
ref={chat.ref}
messages={chat.messages}
onMessagesChange={chat.setMessages}
emptyState={<p>Ask me anything…</p>}
toolbar={<span className="hint">Shift + Enter for a new line</span>}
/>| Prop | What it fills |
|---|---|
emptyState | the chat before the first message |
aboveComposer | the space above the composer |
toolbar | the composer’s bottom row, not drawn when you pass composer (The composer toolbar) |
composer | the composer, replaced by yours |
renderBubble(message) | each message, drawn by you (Bubble) |
The component takes these and no children.
Props and callbacks
Section titled “Props and callbacks”<AparteChat id="support" className="flex-1 min-h-0" loading={isFetchingHistory} attachments />The ones you reach for: placeholder, disabled and submitOnEnter; centerWhenEmpty;
loading, for a wait of your own; isTyping and typingText, a waiting line you drive yourself;
overlayComposer; attachments, off by default (Attachments);
elicitation={false} when you draw the questions yourself; conversationId, config and
layoutTransitionMs, how long the list holds its layout after a conversation swap. The root takes
className and style. id is the chat’s id, which every event carries: for two chats on one page,
each answered by its own client, give each an id and its client scopeToTargetId with it. The
callbacks — onMessageSent, onAction, onMessagesChange, onMessageAppended, onTypingChange
and onConversationCreated — are beside the other frameworks’ in
Wrapper surface.
Control it from code
Section titled “Control it from code”chat.ref, and the helpers useAparteChat returns beside it, are the chat’s imperative API:
appendMessage, addSegment, injectTokenStream, scrollToBottom, focusInput and the rest, to
drive the chat from a loop of yours (Your own loop).
useConversationManager() lists, creates, archives and pins saved conversations once you call
init(adapter) with your storage adapter — and the chat’s config as second argument, if it has
one — and the chat switches between them through conversationId
(Save conversations).
Any element, typed
Section titled “Any element, typed”<aparte-select searchable="" placeholder="Pick a model">
<aparte-option value="gpt-4o-mini">GPT-4o mini</aparte-option>
</aparte-select>Importing from @aparte/react types every element of core as JSX: the attribute names are the
HTML ones, a presence attribute takes '' to be on and null or undefined to be off, and events
are read by ref, typed through the DOM (Elements, typed). A plugin’s
element is typed by the plugin, and one of yours by you
(your own element). <AparteUi> mounts any
tag and sets its props; it forwards core’s events, so name your element’s own in events. A ref to
it gives getElement() and callMethod(name, ...args):
<AparteUi name="my-token-counter" props={{ 'data-budget': '8000' }} events={['token-count']} onElementEvent={(e) => setCount(e.detail)} />Vitest runs on Node, where @aparte/core resolves to its entry without elements, so a tag never
upgrades under jsdom: alias it to @aparte/core/browser
(Testing your components).
A whole chat site in React — the conversation sidebar, the header, settings, tools and the scripted model — is the React example.