Frontend
Rust for Rust, JS for JS: introducing Ahoi
Acheul DEV Community
1 views
Why I built Ahoi
When you want to manage data state in Rust/Wasm inside a web app, there have
been two main options.
Option 1: hand-roll the bridge between Rust and JS. The reactivity does not
live on the Rust/Wasm side, so you usually end up reimplementing the state layer
on both sides.
Option 2: use a Rust frontend framework such as Dioxus, Sycamore, or Leptos.
I like these a lot, but there is a recurring pain point: sometimes JS is just better for
web dev. The large ecosystem of JS UI components is out of reach, and some
HTML/JS native features like event handling feel awkward from a Rust framework.
I kept zigzagging between these two paths. Then the idea for Ahoi came up: go
"Rust for Rust, JS for JS". Keep reactive state management in Rust, and build a
thin bridge to communicate between Rust and JS. Then you can use whatever JS
framework you want for the UI while the core data state stays reactive in Rust.
What Ahoi is
Ahoi has two parts:
A Rust crate: the core reactivity engine.
npm packages: thin bridges, one per supported JS framework (SolidJS,
React, Vue, Svelte).
I built the reactivity engine myself, but I owe a lot to pioneers like Dioxus,
Leptos, and Sycamore.
A quick look
A counter is small, but it shows the whole round trip: a value pushed from
Rust, a write sent back, a command, and a derived value recomputed in Rust.
On the Rust side, state lives in a reactive Stock, and a key enum declares
what JS is allowed to subscribe to. Each #[ret(..)] is the type JS gets back.
#[derive(Stock, Serialize, Deserialize)]
pub struct State {
count: i32,
}
#[derive(Rets, Serialize, Deserialize)]
pub enum Hail {
#[ret(i32)]
Count,
#[ret(i32)]
Doubled,
}
fn run_hail(key: Hail) -> JsValue {
let state = use_context::<Stock<State>>().unwrap();
match key {
// read-write: JS can write straight back into the stock
Hail::Count => state.count().set_hail::<Converter>(),
// read-only, recomputed only when `count` actually changes
Hail::Doubled => state.count().memo(|c| *c * 2).set_read_hail::<Converter>(),
}
}
A Tell is a command: JS asks Rust to do something, and Rust owns the logic.
#[derive(Rets, Serialize, Deserialize)]
pub enum Tell {
#[ret(i32)]
Increase,
}
fn run_tell(tell: Tell) -> JsValue {
let state = use_context::<Stock<State>>().unwrap();
match tell {
Tell::Increase => {
let mut count = state.count().write();
*count += 1;
serde_wasm_bindgen::to_value(&*count).unwrap()
}
}
}
On the JS side those keys resolve to fully typed signals of the host framework.
SolidJS here, but the shape is the same for React, Vue, and Svelte:
import { usePier } from "./bridge";
export default function Counter() {
const pier = usePier();
const [count, setCount] = pier.hail("Count"); // writable signal, () => number
const doubled = pier.readHail("Doubled"); // read-only, recomputed in Rust
return (
<div>
<p>count: {count()} · doubled: {doubled()}</p>
<button onClick={() => setCount(count() + 1)}>+1 (write)</button>
<button onClick={() => pier.tell("Increase")}>+1 (tell)</button>
</div>
);
}
Keys are plain values ("Count", no constructors), and count() is number,
not unknown. Ahoi is not a Rust to TypeScript converter: bring your own
(ts-rs, Tsify, ...). Ahoi only adds what those cannot know, which is what each
key returns.
The one-time bridge wiring (wasm init, PierProvider, type export) is covered
in the Quick Start.
Try it
GitHub: https://github.com/acheul/ahoi
Book and live demos: https://acheul.github.io/ahoi/
crates.io: https://crates.io/crates/ahoi
npm: https://www.npmjs.com/package/@acheul/ahoi-js
This is an early release. Feedback and issues are very welcome.
Read original: https://dev.to/acheul_cb82668412b68f7670/rust-for-rust-js-for-js-introducing-ahoi-3k82
← Previous
How a University Student Built a 64+ Client-Side Utility Suite in 30 Days (React, WASM & AI)
Next →
HypeBESTIE: My Free Sincere Compliment Engine
Related
C
Checking If a Business's Google Profile Actually Matches Its Own Website
Frontend
0
DEV Community
D
"Diagrams in Confluence: draw.io, Mermaid, PlantUML or an attached SVG"
Frontend
0
Dev.to (EN Zone)
P
Posting from a shed with one bar of signal: an offline write queue in plain JS
Frontend
0
Dev.to (EN Zone)
W
What's actually inside an image's DPI tag (and why changing it is lossless)
Frontend
0
Dev.to (EN Zone)
Comments0
No comments yet — be the first