The JavaScript SDK is in pre-release and not on npm yet. Install it from source.
FolkwaysDocs

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:

realtime.ts
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

ChatRoom.tsx
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.messages holds 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 an error and a retry() that resends it safely.
  • Use m.id as 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.

On this page