PlayMesh/Docs
HomeGitHubnpm

Using with React

A shared client, event subscriptions with cleanup, and live presence/state hooks.

One client, shared

Create the client once in a module and import it everywhere - not inside a component, where re-renders would recreate it.
lib/playmesh.ts
import { PlayMeshClient } from '@playmesh/client';

export const client = new PlayMeshClient({
  url: import.meta.env.VITE_SERVER_URL,
  auth: async () => ({ token: await getAccessToken() }),
});

export const ready = client.connect(); // one connection for the whole app

Subscribing to events

Subscribe in an effect and return the matching off as cleanup, so handlers do not stack up across re-mounts.
function useServerEvent(event: string, handler: (payload: unknown) => void) {
  useEffect(() => {
    client.on(event, handler);
    return () => { client.off(event, handler); };
  }, [event, handler]);
}

// In a component
useServerEvent('world-event', payload => setWorldEvent(payload));

Live presence and state

Lifecycle handlers (onPresence, onStateChange, onChat, onDisconnect, ...) are add-only - there is no way to remove them, so never register them inside a component. Register one of each at module scope and fan out to component subscribers:
lib/playmesh.ts
const subscribers = new Set<() => void>();
const notify = () => subscribers.forEach(fn => fn());

// Add-only handlers: registered exactly once, at module scope
client.onPresence(notify);
client.onStateChange(notify);

export function subscribe(fn: () => void) {
  subscribers.add(fn);
  return () => { subscribers.delete(fn); };
}
Hooks then subscribe to the local store, which does support removal. presenceOf and stateOf return a fresh object on every call, so setting them into React state always triggers a re-render.
function usePresence(instancePath: string) {
  const [presence, setPresence] = useState(() => client.presenceOf(instancePath));

  useEffect(() => {
    const update = () => setPresence(client.presenceOf(instancePath));
    update(); // sync in case it changed before the effect ran
    return subscribe(update);
  }, [instancePath]);

  return presence; // { count, users } | undefined
}

function useInstanceState(instancePath: string) {
  const [state, setState] = useState(() => client.stateOf(instancePath));

  useEffect(() => {
    const update = () => setState(client.stateOf(instancePath));
    update();
    return subscribe(update);
  }, [instancePath]);

  return state; // public state replica
}

Connection status UI

Same pattern for connection status - the add-only handlers write to module state, components read it through the store:
lib/playmesh.ts
export let status: 'connected' | 'reconnecting' | 'kicked' = 'connected';

client.onDisconnect(() => { status = 'reconnecting'; notify(); });
client.onReconnect(() => { status = 'connected'; notify(); });
client.onKick(() => { status = 'kicked'; notify(); });
function useConnectionStatus() {
  const [current, setCurrent] = useState(status);
  useEffect(() => subscribe(() => setCurrent(status)), []);
  return current;
}
Only on(event, handler) / off(event, handler) for application events support removal. Everything named on<Something> is add-only and belongs at module scope.