@opencx/widget-react-headless when your application owns the chat UI, rendering, navigation and persistence. For the standard widget with a few custom slots, use React components.
This reference targets Widget 5.0.0. Check publication and matching package versions before installing.
Required setup
WidgetProvider requires a nonempty components array containing fallback. It checks this when the provider renders, even if your own message list does not use the registry. Unlike the standard <Widget>, it does not supply default renderers or browser storage.
widget-components.tsx
Support.tsx
Render and send polling messages
The following component renders persisted text messages and sends a message through the same widget session. It is a minimal polling surface; add session navigation, contact collection, attachments, system-message handling and your required error states before shipping.CustomWidget.tsx
type. User messages have content; AI and human messages have data.message; system messages have a subtype and their own payload. Narrow the type before reading data.action. Do not assume every message has data.content.
Provider props
Use stable renderer and storage objects. Update
options.user.token on the existing provider for renewal; do not key the provider by the token string, which would remount it unnecessarily. See identity renewal.
Hooks reference
Use the declarations from your installed version for full signatures. The table below lists the entry points and the state they own.Launcher and layout providers
Wrap consumers ofuseWidgetTrigger() in WidgetTriggerProvider. It provides isOpen and setIsOpen for an external launcher. useWidgetLayout() requires WidgetLayoutProvider inside WidgetProvider and exposes Companion layout state. Neither provider creates a launcher or panel UI for a headless client.
useCompanionChats() exposes Companion session management. useComposerDraft() keeps a composer draft with that session state. These are presentation helpers; enabling them does not opt into streaming or page access.
Session status and handoff
ObserveuseSessions().sessionState.session to render current session state. There is no onHandoff widget option. hooks.onMessageReceived runs when a new non-user message arrives, with that message and its session; it does not guarantee a callback at the moment of transfer. Do not use it as a replacement for an immediate transfer notification.
Ask whether a reply helped
useMessages().messagesState.lastAIResMightSolveUserIssue is true when the latest AI reply might have solved the visitor’s issue. Use it to show your own “This was helpful” / “I need more help” prompt or to start a feedback request.
- It resets to
falsewhen the visitor sends a message and is set once the AI reply completes, with polling and with streaming. Streaming sets it from widget version 5.4.1. - It stays
falsewhen the session is handed off to a teammate, when the visitor stops the reply, and when a newer message is already waiting for its turn. - To change when the AI counts a reply as possibly solving the issue, write your own rule under Might Solve User Issue in Settings → Autopilot.
Add streaming deliberately
Setoptions.streaming: true only after your UI handles streaming and your organization supports it. Omitted or false keeps polling. useAgentChatUi() returns inactive defaults when the streaming engine is not active.
Apply your configured presentation restrictions to both live items and saved history. The exported
applyPresentation helper applies the supported visibility policy. The standard widget implements this rendering; a headless UI must supply it.
Declare capabilities.richReplies, capabilities.structuredQuestions or capabilities.pageEffects only after you implement that interaction. Leave unsupported flags off so the agent uses ordinary replies. Page actions require all relevant organization and embed permissions plus a consent implementation; simply advertising pageEffects does not implement one.
For personal connections, capabilities.connections: true is also required. Use useConnection() only after implementing the corresponding UI and signed account flow. See Connections.
Stream in React Native
Streaming in a React Native app needs Widget 5.1.0 or later and Expo SDK 52 or later. Install matching widget packages and the two polyfills the stream reader needs:app/_layout.tsx), before anything that loads the widget:
polyfills.ts
app/_layout.tsx
fetch. React Native’s built-in fetch cannot read a reply while it arrives, so streamingFetch takes the one from expo/fetch. Every other widget request keeps using the global fetch.
SupportChat.tsx
resumeInterruptedTurn when the app returns to the foreground, and from your connectivity listener when the network returns. It does nothing unless the last reply was cut off.
ResumeOnForeground.tsx
<ResumeOnForeground /> inside WidgetProvider, next to your chat UI.
Voice dictation is not available in React Native.
Custom action-result components
Match an action’s component key and validate its result before rendering. For example:BalanceMessage.tsx
{ key: 'getAccountBalance', component: BalanceMessage } to the registry for an action that emits that key. components is a message/action renderer registry. It does not register a new type in the agent-generated rich-reply catalog. Generic rich forms and customer-defined rich-reply catalogs are not provided by this release.
Verify your headless UI
- Test first contact creation, existing and resolved sessions, error UI, attachments and human replies.
- Test persistence after reload with your storage adapter.
- Renew a verified token for the same user, then switch accounts and log out. Confirm the old session is not rendered under another identity.
- If streaming is enabled, test live output, Stop, retry, queued messages and history without duplicate rows.
- In React Native, background the app mid-reply and bring it back, and drop and restore connectivity mid-reply. Confirm the reply finishes once.
- If you replace a standard renderer, verify every capability you advertise, including visitor approval and stale page targets.