Skip to main content
Use @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
Pass a storage adapter if identity and session selection should survive a page reload:
Support.tsx
Mount this in your client-side app. Your UI must handle contact collection when configured, empty history, attachments, send errors, resolved sessions and identity changes. The snippets above establish the provider contract; they do not supply that application UI.

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
Persisted messages are discriminated by 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 of useWidgetTrigger() 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

Observe useSessions().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 false when 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 false when 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

Set options.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:
Add the polyfills in their own file and import it first in your app entry (with Expo Router, the root app/_layout.tsx), before anything that loads the widget:
polyfills.ts
app/_layout.tsx
Pass a streaming 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
Resume replies on your app’s own signals. React Native has no tab visibility, so call 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
Render <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
Add { 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.
See Migrate to v5 for the compatibility checklist and Configuration reference for the shared options.