# Share an order across islands with the store

**For:** developers who have built a page with a loader and placed an island and want several islands on one page to agree on the same values.

You'll build a coffee order page. A loader seeds the size, the price and the quantity. One island picks the size, one steps the quantity and one shows the total. None of them pass props to each other: they read and write keys in the store, the server renders the first paint from the same keys and a derived key computes the total on both sides.

Before you start: you need Rust with Cargo. Install snapfire_fsr_cli from crates.io with `cargo install snapfire_fsr_cli`.

## Set up the project

Start a bare application and add React:

```sh
fsr new coffee
cd coffee
fsr use react
```

The route for this tutorial is `/order`. Make its directory now:

```sh
mkdir -p app/routes/order app/src/ui app/src/store
```

## Name the keys

A key is a branded string. Name every key the page shares in one module, so an island and a loader cannot disagree on the spelling or the type:

```ts title="app/src/store/order.ts"

export const size = key<string>("order/size");
export const price = key<number>("order/price");
export const qty = key<number>("order/qty");
export const total = key<number>("order/total");
```

## Seed the keys from the loader

The page loader returns the bag of beans and its sizes. Its `store` export turns that data into the first values of the keys. The server renders the page from these values and the browser starts from the same ones, so the first paint and the hydration agree:

```ts title="app/routes/order/page.loader.ts"

export const SIZES = [
  { label: "250g", price: 12 },
  { label: "500g", price: 22 },
  { label: "1kg", price: 40 },
];

export async function load(_ctx: Ctx) {
  return { name: "Kiambu AA", sizes: SIZES };
}

export const store = ({ data }: { data: { sizes: typeof SIZES } }) => ({
  "order/size": data.sizes[0].label,
  "order/price": data.sizes[0].price,
  "order/qty": 1,
});
```

Every key the loader seeds is a starting point. An island may write any of them afterwards and its write stands until the page loads again.

## Compute the total once, for both sides

`derive` names a key that is computed from others. The browser recomputes it whenever a source changes. The server computes it the same way before it renders, so the total is right on the first paint:

```ts title="app/src/main.ts"

derive(total, [price, qty], (read) => (read(price) ?? 0) * (read(qty) ?? 0));
```

`app/src/main.ts` is the application's entry module. The server runs its `derive` when it renders and the browser runs it when the module loads.

## Pick the size

The first island reads the size and writes two keys at once when a button is clicked. `transaction` collapses the two writes into one notification, so a reader of both sees them together:

```tsx title="app/src/ui/Sizes.tsx"

export function Sizes({ sizes }: { sizes: { label: string; price: number }[] }) {
  const [chosen] = useStore(size, sizes[0].label);
  return (
    <div className="sizes">
      {sizes.map((s) => (
        <button
          key={s.label}
          className={s.label === chosen ? "size chosen" : "size"}
          onClick={() => transaction(() => { set(size, s.label); set(price, s.price); })}
        >
          {s.label} ${s.price}
        </button>
      ))}
    </div>
  );
}
```

`useStore(size, fallback)` reads like `useState`. The fallback is what the component shows while nothing has set the key, which never happens on this page since the loader seeds it, but the component is honest about the case either way.

## Step the quantity

The second island writes the quantity and tells the server about it. `optimistic` sets the key at once, runs the action and puts the key back if the action fails:

```ts title="app/routes/order/page.action.ts"

export type SetQty = { qty: number };

export const POST = action(async ({ input, session }: ActionCtx<SetQty>) => {
  session.order = { ...session.order, qty: input.qty };
  return { qty: input.qty };
});
```

```tsx title="app/src/ui/Stepper.tsx"

const setQty = action("order.setQty");

export function Stepper() {
  const [count] = useStore(qty, 1);
  const step = (next: number) => optimistic(qty, next, () => setQty({ qty: next }));
  return (
    <div className="stepper">
      <button onClick={() => step(Math.max(1, count - 1))} aria-label="one fewer">-</button>
      <span>{count}</span>
      <button onClick={() => step(count + 1)} aria-label="one more">+</button>
    </div>
  );
}
```

The action's answer revalidates the route, so the loader runs again and seeds the keys again. A key the visitor wrote keeps the visitor's value.

## Show the total

The third island only reads. It reads the derived key like any other:

```tsx title="app/src/ui/Total.tsx"

export function Total() {
  const [amount] = useStore(total, 0);
  return <p className="total">Total: ${amount}</p>;
}
```

## Place the islands on the page

The page places the three islands. Each is its own root in the browser; the store is what joins them:

```tsx title="app/routes/order/page.tsx"

export default function Order({ data }: PageProps<"routes/order/page">) {
  return (
    <main>
      <h1>{data.name}</h1>
      <Island when="load"><Sizes sizes={data.sizes} /></Island>
      <Island when="load"><Stepper /></Island>
      <Island when="load"><Total /></Island>
    </main>
  );
}
```

## Run it

```sh
fsr dev app
```

Open `/order`. The page arrives with `250g`, a quantity of `1` and `Total: $12` already in the markup; nothing waits for JavaScript to show a number. Click `1kg`: the size and the price move together and the total reads `$40`. Click `+` twice: the quantity reads `3` and the total `$120`. The session holds the quantity the two round trips set. Reload the page: the loader seeds the keys again and the quantity the visitor chose is still there.

## Test it

A page spec renders the route with its loader and drives the islands the way a visitor would:

```tsx title="app/tests/order/page.spec.tsx"

it("moves the total when the size and the quantity change", async () => {
  await render(Order, { route: "/order" });
  expect(screen.getByText("Total: $12")).toBeInTheDocument();
  await userEvent.click(screen.getByRole("button", { name: "1kg $40" }));
  expect(screen.getByText("Total: $40")).toBeInTheDocument();
  await userEvent.click(screen.getByRole("button", { name: "one more" }));
  expect(screen.getByText("Total: $80")).toBeInTheDocument();
});
```

```sh
fsr test app
```

## What you built

Three islands, no props between them. The loader seeds the store, the server renders the first paint from the seeds, a derived key computes the total on both sides and the islands write the keys they own. Adding a fourth island that reads the price is one `useStore` away.
