Primitive to create a web socket connection
| Stage | Category | Version | Last Updated | Demo |
|---|---|---|---|---|
| 3 | Network | 2.0.0-next.3 (next) | Aug 12, 2026 | Demo → |
npm i @solid-primitives/websocket@nextPrimitives to help establish, maintain, and operate WebSocket connections in Solid.
Connection primitives
makeWS— raw WebSocket with a buffered send queue (manual cleanup)createWS— same, but closes on owner disposalcreateWSState— reactivereadyStatesignal (0–3)makeReconnectingWS— auto-reconnects on involuntary close (manual cleanup)createReconnectingWS— same, but closes on owner disposalmakeHeartbeatWS— wraps a reconnecting WS with a heartbeat/pong watchdog
Message primitives
createWSMessage— reactive signal for the latest received messagewsMessageIterable— bufferedAsyncIterableover WS messagescreateWSData— async memo compatible with<Loading>,isPending, andlatestcreateWSStore— reactive store driven by WS message patches
Connection primitives
makeWS
Sets up a WebSocket with a buffered send queue. Messages sent before the connection opens are queued and flushed on open. Does not close on cleanup — use createWS for that.
const ws = makeWS("ws://localhost:5000");createEffect( () => serverMessage(), msg => ws.send(msg),);onCleanup(() => ws.close());createWS
Same as makeWS, but registers ws.close() with onCleanup.
const ws = createWS("ws://localhost:5000");createEffect( () => serverMessage(), msg => ws.send(msg),);createWSState
Returns a reactive Accessor<0 | 1 | 2 | 3> tracking the WebSocket's readyState.
const ws = createWS("ws://localhost:5000");const state = createWSState(ws);const labels = ["Connecting", "Open", "Closing", "Closed"] as const;
return <p>Status: {labels[state()]}</p>;createWSMessage
Returns a reactive Accessor<T | undefined> that holds the most recently received message. Starts as undefined.
const ws = createWS("ws://localhost:5000");const message = createWSMessage<string>(ws);
return <p>Last message: {message()}</p>;Note — "latest wins" semantics.
createWSMessageuses a signal internally. In Solid 2.0, signal writes are batched: if two messages arrive before the reactive flush, only the second is seen by effects. This is fine for "current state" displays, but if your protocol can burst messages and you need to process every one, usewsMessageIterableorcreateWSDatainstead.
makeReconnectingWS
Returns a WebSocket-shaped proxy that transparently opens a new underlying connection whenever the server closes it involuntarily.
const ws = makeReconnectingWS("ws://localhost:5000", undefined, { delay: 3000, retries: Infinity });createEffect( () => serverMessage(), msg => ws.send(msg),);onCleanup(() => ws.close());createReconnectingWS
Same as makeReconnectingWS, but closes on owner disposal.
makeHeartbeatWS
Wraps a ReconnectingWebSocket to send a periodic heartbeat. If no response arrives within wait ms the connection is force-reconnected.
const ws = makeHeartbeatWS(createReconnectingWS("ws://localhost:5000"), { message: "ping", interval: 1000, wait: 1500,});Async message primitives
These three primitives leverage Solid's async reactivity — createMemo with AsyncIterable, <Loading> boundaries, isPending, and latest — to provide a more powerful and correct model for WebSocket data.
wsMessageIterable
function wsMessageIterable<T = string>(ws: WebSocket): AsyncIterable<T>;The foundational building block. Returns a buffered AsyncIterable<T> over a WebSocket's message stream. Messages that arrive while a consumer is busy are queued; none are dropped. The event listener is removed automatically when the iterator's return() method is called (Solid does this on memo disposal).
// Compose freely with any Solid 2.0 async primitive:const latestQuote = createMemo(async function* () { for await (const raw of wsMessageIterable<string>(ws)) { yield JSON.parse(raw) as Quote; }});Works correctly with makeReconnectingWS — event listeners are re-attached to each new underlying connection, so the iterable survives reconnects transparently.
Why this doesn't drop messages: Unlike createWSMessage, each yielded value triggers its own reactive flush. Messages that arrive while an earlier one is being processed are buffered and drained in order, so no message is skipped.
createWSData
function createWSData<T = string, U = T>( ws: WebSocket, options?: { transform?: (msg: T) => U },): Accessor<U>;An async memo wrapping wsMessageIterable. Suspends the nearest <Loading> boundary until the first message arrives; subsequent updates work with isPending and latest. The optional transform is applied to each raw message before the memo value is updated.
const ws = createReconnectingWS("wss://prices.example.com");const price = createWSData<Quote>(ws, { transform: JSON.parse });
return ( <Loading fallback={<p>Waiting for first quote…</p>}> <p class={isPending(() => price()) ? "stale" : ""}> Bid: {price().bid} / Ask: {price().ask} </p> </Loading>);Comparison with createWSMessage:
createWSMessage | createWSData | |
|---|---|---|
| Drops burst messages | Yes | No |
Works with <Loading> | No | Yes |
isPending() support | No | Yes |
latest() support | No | Yes |
Returns undefined before first message | Yes | No — throws (suspends) |
| Best for | Simple last-value display | State-source WS, real-time feeds |
createWSStore
function createWSStore<S extends object, T = string>( ws: WebSocket, options: { initial: S; patch: (draft: S, msg: T) => void; },): [store: Store<S>, setStore: StoreSetter<S>];A reactive store driven by WebSocket messages as incremental patches. patch is called for each incoming message with a mutable draft of the store — mutate it directly, no return value needed. The event listener is removed on owner disposal.
interface AppState { users: string[]; status: "connecting" | "online" | "offline";}
const ws = createReconnectingWS("wss://app.example.com");const [state] = createWSStore<AppState, string>(ws, { initial: { users: [], status: "connecting" }, patch(draft, msg) { Object.assign(draft, JSON.parse(msg)); },});
return <p>Users online: {state.users.length}</p>;You can also use the returned setter to apply local updates:
const [state, setState] = createWSStore(ws, { initial: { count: 0 }, patch });
// imperative write from a button click, etc.setState(s => { s.count = 0;});Composing with action (request/response pattern)
For protocols with correlated request/response over a shared WebSocket, Solid action is used:
const queryServer = action(function* (payload: RequestPayload) { const id = crypto.randomUUID();
setOptimisticState(draft => { draft.loading = true; });
ws.send(JSON.stringify({ ...payload, id }));
const response: ResponsePayload = yield new Promise(resolve => { const handler = (e: MessageEvent) => { const msg = JSON.parse(e.data); if (msg.id === id) { ws.removeEventListener("message", handler); resolve(msg); } }; ws.addEventListener("message", handler); });
refresh(() => serverData()); return response;});Type reference
type WSMessage = string | ArrayBufferLike | ArrayBufferView | Blob;
type WSReconnectOptions = { delay?: number; // ms between reconnect attempts — default: 3000 retries?: number; // max reconnect attempts — default: Infinity};
type ReconnectingWebSocket = WebSocket & { reconnect: () => void; send: ((msg: WSMessage) => void) & { before?: () => void };};
type WSHeartbeatOptions = { message?: WSMessage; // default: "ping" interval?: number; // ms between heartbeats — default: 1000 wait?: number; // ms to wait for pong before reconnecting — default: 1500};
type WSDataOptions<T, U = T> = { transform?: (msg: T) => U; // map each raw message before the memo value updates};
type WSStoreOptions<S, T = string> = { initial: S; // starting store state patch: (draft: S, msg: T) => void; // mutate the draft for each incoming message};Changelog
See CHANGELOG.md