@dotwire/react
@dotwire/react wraps @dotwire/client in a provider and hooks for messages, typing, presence, and connection state.
Install
npm install @dotwire/react @dotwire/client reactreact (18 or later) and @dotwire/client are peer dependencies. Every export is client-only, and the bundle carries a "use client" banner for React Server Components frameworks.
Quickstart
import { DotwireProvider, useRoom, useTyping, usePresence, useConnectionState } from '@dotwire/react';
const dotwireOptions = {
baseUrl: 'https://chat.yourdomain.com',
tokenUrl: '/api/auth/token'
};
function App() {
return (
<DotwireProvider options={dotwireOptions}>
<Room roomId="a0000000-0000-0000-0000-000000000001" />
</DotwireProvider>
);
}
function Room({ roomId }: { roomId: string }) {
const connectionState = useConnectionState();
const { messages, status, hasMore, loadOlder, send } = useRoom(roomId, { history: 50 });
const { typingUsers, notifyTyping, stopTyping } = useTyping(roomId);
const { present } = usePresence(roomId);
return (
<div>
<p>Connection: {connectionState}</p>
<p>Room: {status}</p>
{hasMore && <button onClick={loadOlder}>Load older</button>}
<ul>
{messages.map((m) => (
<li key={m.seq}>
#{m.seq} {m.senderId}: {m.content}
</li>
))}
</ul>
<p>Typing: {typingUsers.join(', ') || '—'}</p>
<p>Present: {present.join(', ') || '—'}</p>
<input
onChange={() => notifyTyping()}
onBlur={() => stopTyping()}
onKeyDown={(e) => {
if (e.key === 'Enter') {
void send(e.currentTarget.value);
e.currentTarget.value = '';
}
}}
/>
</div>
);
}
React escapes text content, so rendering m.content as a child is safe. Don't render message content as HTML.
DotwireProvider
<DotwireProvider options={dotwireOptions}>...</DotwireProvider>
// or, if you already manage a DotwireClient yourself:
<DotwireProvider client={existingClient}>...</DotwireProvider>
- With
options, the provider constructs and disposes the client for you.optionshas the same shape asnew DotwireClient(options); see client options. Construction happens in an effect, so the provider is safe to render on the server. Pass a stableoptionsobject, such as a module-level constant or a memoized value: a new object on every render tears down and rebuilds the client. - With
client, the provider never constructs or disposes anything. You own that client's lifecycle, including callingclient.dispose().
Hooks
| Hook | Returns |
|---|---|
useDotwireClient() | The DotwireClient, or null until the provider's effect has created one. It's always null during server rendering. |
useConnectionState() | The live ConnectionState: 'disconnected', 'connecting', 'connected', 'reconnecting', or 'disconnecting'. |
useRoom(roomId, { history = 50 }) | { messages, status, error, hasMore, loadOlder, send } |
useTyping(roomId) | { typingUsers, notifyTyping, stopTyping } |
usePresence(roomId) | { present } |
useRoom
useRoom subscribes for the lifetime of the component, loads the first page of history, and applies live messages, retractions, and revocation.
messagesis sorted byseqand deduplicated.statusstarts as'connecting'and becomes'ready'once the subscribe resolves. It becomes'revoked'if the user's membership is revoked, or'error'if subscribe or send throws.loadOlder()fetches the previous page withbeforeSeqand prepends it.hasMoresays whether an older page may exist.send(content)wrapssendMessage.
useTyping
A user leaves typingUsers when they send an explicit "stopped typing", or after about 3.5 seconds without an update.
usePresence
present is built from the joined and left deltas received since the hook mounted. There is no presence snapshot on the server, so a component that mounts after others joined doesn't see them until the next delta for that room.
Participants
There is no participants hook. Get the client with useDotwireClient(), call client.room(roomId).getParticipants() once it isn't null, and resolve the ids through your host's profile API. See Participants and presence.
Sharing a room between hooks
You can mount useRoom, useTyping, and usePresence together for the same room. The package counts how many hooks watch each room and leaves the room only after the last one unmounts.
Types
@dotwire/react re-exports these types from @dotwire/client, so you don't need a direct dependency just for typing: ConnectionState, DotwireClient, DotwireClientOptions, HistoryPage, MembershipRevoked, MessageRetracted, PresenceDelta, RoomMessage, SendAck, and TypingNotification.