Build a counter with no JavaScript
You'll add a button that works. The browser downloads no component code to make it work. Clicking it sends the event to Rust, Rust runs the handler and sends back the markup that changed.
Before you start
Straight from crates.io. No Node, no package manager.
cargo install snapfire_compiler
cargo install snapfire_fsr_cli
fsr --version
Every command and screenshot on this page was captured with fsr 0.x.
Start from the app you made in 010. A fresh fsr new hello --with react works too; the --with is what puts React in, since a plain fsr new writes no framework.
Write a normal component
Nothing here is special. A useState, two buttons, the handlers you would write anywhere.
import { useState } from "react";
export function Counter({ start }: { start: number }) {
const [n, setN] = useState(start);
return (
<div className="counter">
<p>Count: {n}</p>
<button onClick={() => setN(n + 1)}>Add one</button>
<button onClick={() => setN(start)}>Reset</button>
</div>
);
}Save that as app/src/ui/Counter.tsx.
Place it as a server island
One wrapper decides where the component runs. mode="server" means the handlers run in Rust.
import { Island } from "@snapfire/fsr-client/react";
import type { RootProps } from "@generated/client";
import { Counter } from "@src/ui/Counter";
export default function Index({ greeting }: RootProps) {
return (
<section className="hero">
<h1>{greeting}</h1>
<Island when="load" mode="server">
<Counter start={0} />
</Island>
</section>
);
}Build it
$ fsr build hello/app
rendered routes/page.tsx#default lowered
src/ui/Counter.tsx#Counter lowered
islands src/ui/Counter.tsx#Counter server 2 handlers
plan generated/plan.sexp 2.1 KB written
typecheck tsc 7.0.2 from cache, clean
2 handlers is the build telling you it understood both onClick bodies well enough to run them without a browser. They are in the plan now, beside the state they touch.
Look at what the browser gets
Run fsr dev hello/app, open the page and read the source. The buttons carry data-sf-on="click:0" and click:1. The region carries data-sf-mode="server". Open the network panel and there's no module for Counter. There is no React root for it either.
Click the button. One request goes to /_sf/island/… and comes back with the new count and the markup around it. The browser patches in what changed. While the request is out the island carries data-sf-pending, which is your hook for styling a busy state.
What a server handler can do
The build has to be able to run the handler in Rust, so a handler is consts and calls to your state setters:
onClick={() => setN(n + 1)}
onClick={() => setN((prev) => prev - 1)}
onChange={(e) => setQty(Number(e.target.value))}e.preventDefault() is allowed and dropped. Anything else is refused at build time with the line it's on, rather than failing quietly at runtime. A component nested inside a server island can't have its own state or handlers either, because the round trip carries exactly one state: the island's.
When to use it
The trade is a round trip per event and no optimistic update. A toggle, a quantity stepper, a filter, a sort order: all fine, all free of JavaScript. A text field somebody types into continuously isn't, because every keystroke would be a request. Change mode="server" to mode="browser" and the same component hydrates as React instead, with the module loaded. The component doesn't change, only the placement does.
Next up: 030. Add a working cart, where the state stops living in a component and starts living on the server for real.