Docs

React

Using WebSockets in React with @foony/realtime, from one shared client to live state in components.

React and WebSockets fit together cleanly once two responsibilities are separated: the SDK owns the socket, reconnection, and delivery, and your components own subscribing in effects and turning messages into state. There are no React bindings to install, the plain SDK is already effect-friendly.

One client for the whole app

Create the client once, in a module, and import it wherever it’s needed:

// src/realtime.ts
import { Realtime } from '@foony/realtime';

export const realtime = new Realtime({
  authCallback: async () => {
    const response = await fetch('/api/realtime/token');
    return response.text();
  },
});

Constructing the client doesn’t open the socket. The first component that subscribes connects it, so there’s no startup cost for pages that never use realtime. One client per app matters because each Realtime instance is one connection against your plan’s limit.

Subscribe in an effect

function ChatLog() {
  const [messages, setMessages] = React.useState<ChatMessage[]>([]);

  React.useEffect(() => {
    const channel = realtime.channels.get('chat:lobby');
    const off = channel.subscribe((message) => {
      setMessages((current) => [...current, message.data as ChatMessage]);
    });
    return off;  // Unsubscribe when the component unmounts
  }, []);

  return <ul>{messages.map((m) => <li key={m.id}>{m.text}</li>)}</ul>;
}

subscribe returns its own unsubscribe function, which is exactly what the effect wants to return. This pattern is also safe under Strict Mode’s double-invoked effects: the second subscribe just adds and removes a listener, and channels.get returns the same channel either way.

Presence as state

The same shape turns presence into a roster:

function OnlineList() {
  const [members, setMembers] = React.useState<Map<string, unknown>>(new Map());

  React.useEffect(() => {
    const channel = realtime.channels.get('chat:lobby');
    return channel.presence.on((event) => {
      setMembers((current) => {
        const next = new Map(current);
        if (event.action === 'leave') {
          next.delete(event.clientId);
        } else {
          next.set(event.clientId, event.data);
        }
        return next;
      });
    });
  }, []);

  return <p>{members.size} online</p>;
}

The listener receives the current members as enter events first, so the roster builds itself with no separate fetch, per Presence.

Publishing

Publishing needs no effect, it’s just an event handler:

<button onClick={() => realtime.channels.get('chat:lobby').publish('chat', { text })}>
  Send
</button>

Where to go next

The token endpoint the client fetches from is a few lines on your backend, see Server auth. If you’re on Next.js, the Next.js quickstart shows both halves in one project. For chat UIs specifically, @foony/chat handles messages, typing, and presence at a higher level.