2. Put state on the server

Stop Hello World, then run:

sh
npm run counter

Open http://localhost:8181 in two tabs. Click either button. Both counters should change.

tsx
import { action, defineApp, page, state } from 'redweb';

@page('/', { shared: true })
class CounterPage {
    @state() count = 0;

    @action()
    increment() { this.count += 1; }

    render() {
        return <main>
            <h1>A counter owned by the server</h1>
            <button rw-click="increment">Count {this.count}</button>
        </main>;
    }
}

const app = defineApp({ pages: [CounterPage] });

await app.run();

@state() marks a value that Redweb should observe. @action() exposes one server method to this page. rw-click="increment" connects the button to that method. {this.count} is enough: Redweb creates the update binding when it renders the expression.

shared: true means visitors share this page instance inside one server process. It does not save the counter to disk or synchronize multiple machines.

Imagine two people looking through different windows at the same scoreboard. The scoreboard lives inside the building; each window displays what the server says.

What changes for a game?

The counter can use Redweb's built-in page-action socket. A game benefits from its own explicit vocabulary: join, move, and resume at /match. We will keep TSX for the HTML, but use a routed game socket for those messages. We will not bolt an action field onto a generic message handler.

Checkpoint: changing server state changes both views. Reloading is not required; restarting the server resets this intentionally in-memory counter.

Download the complete project Setup, optional configuration, and limits