Skip to content

Clients never write state. They call commands, and the host runs them. A command is a typed function call with arguments and a result, so the client gets autocomplete and the host keeps every rule in one place.

The screen doesn't wait. A client can predict a command's effect and show it at once. When the host's answer arrives, the prediction is replaced by the real state. If the host says no, the prediction disappears.

A command in three steps. One: the client calls increment and shows its prediction at once. Two: the host runs the handler and sends the new state to every client; the prediction is dropped. Three: the host returns the command's result to the caller.Clientshows +1 at onceHostruns the handlerOther clientssame state1increment()2new state3result2new state

Try it

Two clients and one host, running in this page. Send +1 from A, then +1 from B. Each client shows its own +1 while the host still holds 0. Then apply, reject and return results, and watch each screen settle.

Two clients, one state
Send from A and B, then apply one command. Watch the other client’s pending +1 rebase.

Client A

connecting
…
Last received count
Ready to send.

Client B

connecting
…
Last received count
Ready to send.

One shared host

Step the host to keep each stage visible.
0
Send +1 from either client to queue a command.
Applying broadcasts state. Returning a result settles only the sender’s promise.
Command log (0)
Real Statewire clients over in-browser SSE test transports. Detach ends an idle client’s stream; reattach reads a fresh snapshot.

Write a command

A handler can reject with a reason. Every write it made before that stands.

The client predicts with a plain function. It runs again whenever fresh state arrives, so it never drifts.

const { state, commands } = useStatewire<{ count: number } | undefined, Commands>({
  transport: StatewireHttp({ url: "/counters/main" }),
  optimistic: (draft, command) => {
    if (command.method === "increment") draft.count += 1;
  },
});

const { count } = await commands.increment();

Long commands

A command can keep running after its first effect. Mark the effect as applied, then stream. The caller's promise resolves when the handler returns; everyone sees the text as it grows.

Other commands keep running while one streams, so a second client can send its own in the middle of a reply.