Skip to content

Quick start

A counter is a small thing, but it exercises the whole bridge: a value pushed from Rust, a write sent back, a command, and a derived value recomputed in Rust.

Pick your framework in the tabs. The choice follows you across the whole book.

lib.rs
#[derive(Stock, Serialize, Deserialize)]
pub struct State {
count: i32,
}
#[derive(TS, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[ts(export)]
pub enum Pier {
Top,
}
wasm_bindgen_enrol_sphere!(@pier, Pier, run_pier, Converter);
fn run_pier(key: Pier) {
match key {
Pier::Top => {
set_js_hail_dispatcher();
let state = Stock::new(State {
count: 0,
});
provide_context(state);
}
}
}
#[derive(Rets, TS, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[ts(export)]
pub enum Hail {
#[ret(i32)]
Count,
#[ret(i32)]
Doubled,
}
wasm_bindgen_enrol_sphere!(@hail, Hail, run_hail, Converter);
fn run_hail(key: Hail) -> JsValue {
let state = use_context::<Stock<State>>().unwrap();
match key {
// read-write: JS can write back into the stock
Hail::Count => state.count().set_hail::<Converter>(),
// read-only, and recomputed only when `count` actually changes
Hail::Doubled => state.count().memo(|c| *c * 2).set_read_hail::<Converter>(),
}
}
#[derive(Rets, TS, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[ts(export)]
pub enum Tell {
#[ret(i32)]
Increase,
}
wasm_bindgen_tell!(Tell, run_tell, Converter);
fn run_tell(tell: Tell) -> JsValue {
let state = use_context::<Stock<State>>().unwrap();
match tell {
Tell::Increase => {
let new_count = {
let mut count = state.count().write();
*count += 1;
*count
};
serde_wasm_bindgen::to_value(&new_count).unwrap()
}
Counter.tsx
import { usePier } from "../../setup/solid/bridge";
export default function Counter() {
const pier = usePier();
const [count, setCount] = pier.hail("Count"); // writable
const doubled = pier.readHail("Doubled"); // read-only memo
return (
<div class="demo">
<p>
count: <b id="count">{count()}</b> · doubled: <b id="doubled">{doubled()}</b>
</p>
<button id="write-count" onClick={() => setCount(count() + 1)}>
+1 (write)
</button>
<button id="tell-increase" onClick={() => pier.tell("Increase")}>
+1 (tell)
</button>
</div>
);
}

Running now, in your browser

The demo above is not a recording. It is this page running the code in the Solid tab, against the Rust in the top panel, compiled to wasm.

Reading the Rust panel from the top:

State holds the data. #[derive(Stock)] generates an accessor per field, so state.count() is a reactive handle on that one field.

Pier::Top sets up a scope. It runs once, provides the state as context, and everything created inside belongs to that scope. When the JS component unmounts, the scope is cleared and all of it goes away.

Hail::Count is a channel. set_hail makes it read-write, so the JS side gets both a value and a setter. Hail::Doubled uses set_read_hail instead: read-only, and its memo only recomputes when count actually changes.

Tell::Increase is a command. It mutates state and returns the new value. The #[ret(i32)] on each variant is what makes that return a number in TypeScript rather than unknown.

The demo has two buttons, and they take different routes.

Write sends the new value straight into the stock. Use it when the JS side already knows what the value should be: a form field, a toggle, a slider.

Tell asks Rust to do something. Use it when the logic belongs in Rust: validation, a computation, anything touching several pieces of state at once.

Both end up in the same place. Writing is shorter, telling keeps rules on the Rust side.

Doubled updated on every click without you wiring anything.

When a write lands, Rust walks only the values that actually depend on what changed, recomputes those, and sends the results to JS in a single batch. One write that touches twenty hails costs one crossing, not twenty.

Read about what Ahoi is if you skipped it, or keep going into the bridge: piers, hails, and tells in full.