Using it with React
Build live UIs with useSyncExternalStore — message lists with sending status, current values and the connection state.
The SDK has no React dependency. Its live values (topic.messages, topic.values(), client.connection) work
directly with React's useSyncExternalStore, and the same pattern works with Svelte stores and Vue refs.
One client for the app
Create the client once, outside your components:
import { Folkways } from '@folkways/realtime';
export const client = new Folkways.Realtime({
url: 'https://rte.example.com',
authUrl: '/api/realtime-token',
});
void client.connect();A chat room
import { useEffect, useState, useSyncExternalStore } from 'react';
import { client } from './realtime';
type ChatEvents = { message: { text: string } };
export function ChatRoom({ roomId }: { roomId: string }) {
const room = client.topic<ChatEvents>(`chat/${roomId}`);
const messages = useSyncExternalStore(room.messages.subscribe, room.messages.getSnapshot);
const [draft, setDraft] = useState('');
useEffect(() => {
const sub = room.subscribe({ from: { last: 50 } });
return () => void sub.unsubscribe();
}, [room]);
return (
<div>
<ul>
{messages.map((m) => (
<li key={m.id}>
<b>{m.publisher}</b> {m.data.text}
{m.status === 'pending' && ' (sending…)'}
{m.status === 'failed' && <button onClick={() => void m.retry?.()}>Retry</button>}
</li>
))}
</ul>
<form
onSubmit={(e) => {
e.preventDefault();
void room.publish('message', { text: draft });
setDraft('');
}}
>
<input value={draft} onChange={(e) => setDraft(e.target.value)} />
</form>
</div>
);
}room.messagesholds the latest 100 stream events while a subscription on the topic is active, plus your own sends right away.- Each entry has a
status:'pending'until the node stores it, then'sent', or'failed'with anerrorand aretry()that resends it safely. - Use
m.idas the React key. A pending send has a temporary id (local:…) that becomes the stored id once confirmed. client.topic()returns the same object for the same name, so it is safe to call during render.- Unsubscribing in the cleanup is cheap: subscriptions to the same topic share one network subscription, and StrictMode's double mount reuses it.
Current values
topic.values(event) is a live Map of kv values by key, or of signals by sender:
function Typing({ roomId }: { roomId: string }) {
const topic = client.topic(`typing/${roomId}`);
const view = topic.values('typing');
const typists = useSyncExternalStore(view.subscribe, view.getSnapshot);
useEffect(() => {
const sub = topic.subscribe();
return () => void sub.unsubscribe();
}, [topic]);
const others = [...typists.values()].filter((m) => m.publisher !== client.session?.identity);
return others.length > 0 ? <p>{others.map((m) => m.publisher).join(', ')} typing…</p> : null;
}Connection state and session
const { state } = useSyncExternalStore(client.connection.subscribe, client.connection.getSnapshot);
const session = useSyncExternalStore(client.sessionView.subscribe, client.sessionView.getSnapshot);
const canPost = session?.can('publish', 'chat/lobby') ?? false;Fast reloads
With a persistent store, topic.messages and topic.values() are filled from the
browser's storage on reload, so the room renders before the connection is up. Those entries have
fromCache: true until the node confirms them.
A complete chat example is in the folkways-js repository.