---
title: "Build a counter"
audience: "anyone new to server islands"
description: "You'll add a working button. The browser downloads no component code for it. Clicking it sends the event to Rust, Rust runs the handler and sends back the…"
snippet_caption: "app/src/ui/Counter.tsx"
snippet: "import { useState } from \"react\";\n\nexport function Counter({ start }: { start: number }) {\n  const [n, setN] = useState(start);\n  return (\n    <div className=\"counter\">\n      <p>Count: {n}</p>\n      <button onClick={() => setN(n + 1)}>Add one</button>\n      <button onClick={() => setN(start)}>Reset</button>\n    </div>\n  );\n}"
---

import { Island } from "@snapfire/fsr-client/react";
import { Install } from "@src/ui/Install";
import { Demo } from "@src/ui/Demo";
import { Counter } from "@src/demos/Counter";

You'll add a working button. The browser downloads no component code for it. Clicking it sends the event to Rust, Rust runs the handler and sends back the markup that changed.

<Install captured="0.x" />

Start from the app you made in [010](https://snapfirers.com/fsr/learn/010-build-your-first-page). A fresh `fsr new hello --with react` works too; `--with react` adds React, since a plain `fsr new` writes no framework.

## Write a normal component

This is an ordinary component: a `useState`, two buttons and the handlers you would write anywhere.

```tsx title="app/src/ui/Counter.tsx"
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

The `Island` wrapper decides where the component runs. `mode="server"` means the handlers run in Rust.

```tsx title="app/routes/page.tsx"
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

```shell-session
$ fsr build hello/app --verbose
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` means the build understood both `onClick` bodies well enough to run them in Rust. They are now in the plan 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"`. The network panel shows no module for `Counter` and there is no React root for it.

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.

<Demo title="The counter from this page, placed as a server island">
  <Island when="load" mode="server">
    <Counter start={0} />
  </Island>
</Demo>

Open the network panel and click it here. Each click is one request to `/_sf/island/…` and no module for `Counter` loads.

## What a server handler can do

The build has to be able to run the handler in Rust, so a handler is `const`s and calls to your state setters:

```tsx
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, so it can't fail silently 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

Each event costs a round trip and there is no optimistic update. A toggle, a quantity stepper, a filter or a sort order works well this way. A text field somebody types into continuously doesn't, because every keystroke would be a request. Take `mode="server"` off and the same component hydrates as React instead, with the module loaded.

Next up: [030. Add a working cart](https://snapfirers.com/fsr/learn/030-add-a-working-cart), where the state moves out of the component and onto the server.
