One shared connection per tab, and a hook over any field. Fields replace their state on every change, so useSyncExternalStore re-renders exactly when something changed.
.env.local
1NEXT_PUBLIC_SOCKET_URL=wss://live.example.comlib/socket.ts
12345678910111213141516171819"use client"
import { useSyncExternalStore } from "react"import { connect, type Socket } from "@slash.tools/socket"import type { Fields } from "./socket-fields"
let socket: Socket<Fields> | null = null
/** One connection per browser tab, opened on first use. */export function getSocket() { socket ??= connect<Fields>(process.env.NEXT_PUBLIC_SOCKET_URL!) return socket}
export function useField<K extends keyof Fields & string>(name: K) { const field = getSocket().field(name) const state = useSyncExternalStore(field.listen, field.get, () => undefined) return [state, field] as const}components/chat.tsx
123456789101112131415161718192021222324252627"use client"
import { useState } from "react"import { useField } from "@/lib/socket"
export function Chat() { const [items, chat] = useField("chat") const [text, setText] = useState("")
return ( <div> {items?.map((item) => ( <p key={item.id}> <b style={{ color: item.by.color }}>{item.by.name}</b> {item.value.text} </p> ))} <form onSubmit={(event) => { event.preventDefault() chat.push({ text }).then(() => setText("")) }} > <input value={text} onChange={(event) => setText(event.target.value)} /> </form> </div> )}Cursors move up to 60 times a second per person, so they skip React state: subscribe once and write each cursor's position straight to its element, with transform only and no CSS transition.
components/cursors.tsx
1234567891011121314151617181920212223242526272829303132333435363738394041"use client"
import { useEffect, useRef } from "react"import { getSocket } from "@/lib/socket"
export function Cursors() { const layer = useRef<HTMLDivElement>(null)
useEffect(() => { const cursor = getSocket().field("cursor") const move = (event: PointerEvent) => cursor.send({ x: event.clientX / innerWidth, y: event.clientY / innerHeight }) addEventListener("pointermove", move)
const off = cursor.subscribe((frames) => { const root = layer.current if (!root) return for (const [id, frame] of Object.entries(frames)) { let dot = root.querySelector<HTMLElement>(`[data-id="${id}"]`) if (!dot) { dot = document.createElement("div") dot.dataset.id = id dot.className = "cursor" dot.style.background = frame.from.color root.append(dot) } dot.style.transform = `translate(${frame.value.x * innerWidth}px, ${frame.value.y * innerHeight}px)` } for (const dot of root.querySelectorAll<HTMLElement>("[data-id]")) { if (!(dot.dataset.id! in frames)) dot.remove() } })
return () => { removeEventListener("pointermove", move) off() } }, [])
return <div ref={layer} className="cursors" aria-hidden />}cursors.css
12.cursors { position: fixed; inset: 0; pointer-events: none; }.cursor { position: absolute; width: 10px; height: 10px; border-radius: 50%; }