# 104. Islands in another framework

The question this chapter answers: how does a Vue component end up on a page the server rendered, what does the build do with a `.vue` file and what does an application that has no React in it look like?

**For:** app developers.

## Templates and islands

The pages, layouts and boundaries under `routes/` are TSX the build lowers and the server renders, which chapter 003 covers. None of it is React: the lowerer reads a template's JSX and the server writes the markup. React is needed only when a component needs the browser for `useState`, a handler or a store read, because then the browser must run it. The build keeps a compiled twin of it for that.

An island written in Vue works the same way with a different runtime. The template places it, the server writes its marker and its props and the browser mounts it with Vue. The recipes example, [`recipes_vue_ts`](https://github.com/excsn/snapfire/blob/main/fsr/examples/recipes_vue_ts/README.md), works this way: every route is a template and every interactive piece is a `.vue` file under `src/ui/`. There is no React in the import map, the vendor tree, the bundle or the type declarations.

## Placing a `.vue` component

A template imports the component as a file and places it inside `Island`:

```tsx
import { Island, type Children } from "@snapfire/fsr/template";
import { Link } from "@snapfire/fsr/components";
import Tonight from "@src/ui/Tonight.vue";

export default function BoxLayout({ children, planned }: { children: Children; planned: number }) {
  return (
    <header className="masthead">
      <nav>
        <Link href="/">Recipes</Link>
      </nav>
      <Island when="load">
        <Tonight count={planned} />
      </Island>
      {children}
    </header>
  );
}
```

The build reads the import and asks `snapfirec-vue` to describe the file: the template as Vue's own parser reads it, the `<script setup>` block and how the template reads each name the script binds. It lowers the component the way it lowers a TSX template, to the same render tree and places it as an island with its props lowered like any other placement. The server writes the component's markup inside the `<sf-i>`, spelled the way Vue's own server renderer spells it, with the props script beside it. The browser hydrates over that markup rather than mounting fresh. Place the same component outside `Island` and the build refuses, naming the tag: a component Vue mounts can only be an island, since Vue's root is the island's.

A component the build cannot read stays foreign: the server writes the `<sf-i>` empty with its props and Vue mounts it fresh. The report says which and why, with the line:

```text
rendered  src/ui/Tonight.vue#default         foreign     src/ui/Tonight.vue:10:1
foreign   src/ui/Tonight.vue:10:1            a class in `<script setup>`
          1 component mounts in the browser for it, written empty by the server
            src/ui/Tonight.vue#default
```

What lowers is the subset a server can evaluate. In `<script setup>`: `defineProps`, with `withDefaults` around it, `ref`, `shallowRef`, `computed` of an arrow, `reactive`, `useStore` from `@snapfire/fsr/client/vue`, a `const` bound to an expression the build reads and functions, which an `@event` can name as its handler. Lifecycle and watch calls are the browser's and are passed over. In the template: interpolation, `v-if`, `v-else-if` and `v-else`, `v-for` over a list with an item and an index, a bound attribute, `:class` as a string, an array or an object, `:style`, `v-show`, `v-html`, `v-text` and a plain `<slot />`. `v-model` on an input, a checkbox, a radio, a textarea, a select or a `.vue` child writes the value Vue's server renderer writes and the browser keeps them in step; a child reads it with `defineModel`. `v-bind` of a whole object, named and scoped slots between `.vue` files and `provide` with `inject` lower too. A component written with the Options API lowers too: `props`, `data`, `computed`, `methods` and a `created` that calls `this.$emit`. A child that calls `emit` in `<script setup>` (or `this.$emit` in `created`) runs the parent's `@event` handler while it sets up, so state the handler writes shows in the parent's markup after the child, as it does under Vue's server renderer. `<Transition>`, `<TransitionGroup>` and `<KeepAlive>` render what Vue writes for them. `<Teleport to="body">` writes its content at the end of the body, where Vue hydrates it. `<Teleport to="#notices">` writes it inside the `<Place name="notices">` a shell, layout or page declares. When the component's own template has an element whose `id` is `notices`, it writes it there instead. Outside that is residue: a component placed inside the template that is not a `.vue` file the script imports, `v-slot` anywhere but on a `.vue` child or a `<template>` directly inside one, a `<Teleport>` to a target that is neither `body` nor `#` and an id, a `<Teleport defer>` (Vue never updates a deferred Teleport it has hydrated) and a `created` that does anything but call `this.$emit`. The residue names its line in the `.vue` file.

Vue's `ref` and `computed` are signals and the build lowers them as state and as a derived value; chapter 102 maps the rest of a signals vocabulary onto the store.

A `.vue` child the script imports renders inline as part of the island, the way Vue renders it: `<Tile :name="item.name" :stock="item.stock">` passes props by their camelCase name and its children fill the child's `<slot />`. An attribute the child does not declare as a prop moves onto the child's root element, as Vue moves it, unless the child sets `inheritAttrs: false`.

A `.vue` component can be placed in server mode too, `<Island mode="server"><ServerVue /></Island>`; the host steps its events the way chapter 102 describes for a React one. The build lowers each `@event` on an element into a handler: a setup function by name or called, an arrow or a statement over `$event`. A handler holds `const`s, `if` and `else`, writes to a `ref` or a `useStore` binding, `n++`, `n.value += by` or `count.value = 10`, calls to other setup functions and calls to actions. A write is seen by what the handler reads after it, as in Vue. `.prevent` and `.stop` are allowed; any other modifier, `emit` and anything else stays in the browser for a browser island and stops the build for a server one, naming the line:

```text
`src/ui/ServerVue.vue#default` cannot be an island in server mode: routes/page.tsx#default places it there, but a handler did not lower: 13:25: `.enter` on `@keyup`, a modifier the build does not lower
```

Integers need a conversion. An integer a contract types as `bigint` reaches the server as an integer and a Vue component that multiplies it by a literal fails the render, the way chapter 100 says a loader must convert one before arithmetic. The recipe page passes `serves={Number(recipe.serves)}` for that reason: the browser handles either type, but the server render fails without the conversion.

`Island`, `island` and `Slot` come from `@snapfire/fsr/template` here rather than from `@snapfire/fsr/client/react`. They are the same placements and the build reads either import. `Link` comes from `@snapfire/fsr/components`, which the build points at the framework of each file that imports it. The template module is the portable form: a file written against it is valid whatever the application serves, typed by the dialect's own declarations when the import map has no React and through React's when it has, where `Children` reads as `ReactNode` and the placements as the React module's. An island written on the template module loads its placements from the client's `template.js`, which the `react` direction maps beside the React module. The React module is the React-only form, with `useStore`, `useLocale` and `useHoisted` that the template module never promises. A file importing it needs React's declarations to type-check. So a layout like this one writes `Children` and keeps working if the application gains React later; a file that wants React's hooks says so by its import.

## The component itself

`Tonight.vue` is an ordinary single-file component: a `<script setup lang="ts">`, a `<template>` and a `<style scoped>`. Nothing in it is written for the server: the build reads it as it stands.

```vue
<script setup lang="ts">
import { ref } from "vue";
import { useStore } from "@snapfire/fsr/client/vue";
import { plannedCount } from "@src/store";

const props = defineProps<{ count: number }>();
const held = useStore(plannedCount, props.count);
const open = ref(false);
</script>

<template>
  <button class="tonight-count" @click="open = !open">{{ held.value }} for tonight</button>
</template>

<style scoped>
.tonight-count { border-radius: 999px; }
</style>
```

`useStore` from `@snapfire/fsr/client/vue` is the store as a Vue ref: the same keyed store the layout's loader seeds and a React island reads with its own `useStore`, so a Vue masthead and a React panel would show the same number. On the server it reads the store the loader seeded, so the count is in the markup. The plan control in the example calls `actions.recipe.$id.plan(...)` from the generated client and writes the store optimistically, the same way the storefront's React button does.

The server writes what Vue's server renderer would write for the same component and props, anchors included: `<!---->` where the panel's `v-if` rendered nothing, `<!--[-->` and `<!--]-->` around a list or a fragment and the `data-v-` stamp of a scoped style on every element. That is what Vue's client walks when it hydrates. It is checked byte for byte against `@vue/server-renderer` in the lowerer's own tests:

```html
<sf-i id="sf-i0" data-sf-module="src/ui/Tonight.vue#default"><div class="tonight" data-v-8ee200ee><button class="tonight-count" aria-label="tonight" data-v-8ee200ee>0 for tonight</button><!----></div><template data-sf-children>Kept in the session cookie. <a href="/tonight">See them</a>.</template></sf-i>
```

The `<template data-sf-children>` after the markup is the island's children. The panel is closed, so the template placed no `<slot />` this render and the server had nowhere to write them; an inert template after the markup carries them instead, which the parser never shows and the scan never reaches. The mounter reads it before Vue hydrates and takes it out of the document, so the slot has its content the moment the panel opens. When the template does place the slot, the children sit inside it in an `<sf-s data-sf-children>` region, which Vue hydrates as an element it rendered.

## What the build does with it

Neither `fsr` nor `snapfirec` compiles Vue. `fsr build` looks for `snapfirec-vue` on `PATH` and asks it to describe every `.vue` file under the source directories in one batch before any route is lowered; `snapfirec` finds the same binary again when it bundles and hands it the same files to compile. The plugin carries Vue's own compiler, run in QuickJS, so no Node is involved. Install it with `cargo install snapfire_vue`. Without it nothing reads the file, so the component is neither lowered nor compiled and the bundle stops on the module the registry imports. A `plugins` row in the report says which binary was missing and the report is printed even though the build failed, since the fix is only clear from both errors together:

```text
❌ `snapfirec-vue` is not on PATH; `cargo install snapfire_vue` puts it there
   needed by "src/ui/Tonight.vue"
❌ "dist/generated/islands.js" imports '../src/ui/Tonight.vue', which resolves to nothing
plugins   `snapfirec-vue` is not on PATH, so no `.vue` component was read or compiled; `cargo install snapfire_vue` puts it there, and until it does the bundle stops on the module the registry imports
```

That is not the `foreign` fallback. A component goes foreign when the plugin read it and the build could not lower what it found, which leaves a module to mount; a missing plugin leaves no module at all.

What comes back is a module and a stylesheet: `dist/src/ui/Tonight.vue`, the compiled module under the component's own name, whose imports the build resolves like any other module's, plus `dist/src/ui/Tonight.vue.css`, the component's `<style scoped>` with its `data-v-` attribute. The template's `import Tonight from "@src/ui/Tonight.vue"` stays as written and the host serves the module as JavaScript. The island registry the build writes registers the module with the Vue mounter:

```ts
import { registerDeferredIsland } from "@snapfire/fsr/client";

const vue = () => import("@snapfire/fsr/client/vue").then((m) => ({ mount: m.vueMounter, patch: m.vuePatcher, unmount: m.vueUnmounter }));

export function registerIslands(): void {
  registerDeferredIsland("src/ui/Tonight.vue#default", { loader: () => import("../src/ui/Tonight.vue").then((m) => m.default), adapter: vue });
}
```

The Vue adapter is imported when a scan first finds one of these islands, which is what keeps a framework off a page that holds none of its islands.

The stylesheets reach the head before the first island mounts. `fsr build` names, under each route's layouts and page, the sheet of every Vue or Svelte component the module reaches through its imports. The host links on a document the ones its route names, after the application's own `styles/*.css`, so a component's rules come later in the cascade than the document's. A navigation adds the next route's sheets before it swaps the page and removes the ones it no longer names. A sheet no route names, a component placed only by its module's name, is linked on every document; the compiler lists every component sheet in `dist/.snapfire-build.json` under `styles`.

Unchanged components are not compiled twice. The plugin's output is cached on the worker's own version, the source, the options and every sibling file the component read, in memory across a watch and on disk in the output directory. `fsr build --verbose` says how much of it the cache answered:

```text
   Plugin:   snapfirec-vue 0.2.1 (@vue/compiler-sfc 3.5.44)
   Plugin cache: 3 of 3 answered
```

A block may point at a sibling, `<style src="./Tonight.css" scoped>`. The plugin asks for the file, the build reads it and sends it. From then on a save to `Tonight.css` recompiles `Tonight.vue`.

## Why the page loads no React

A template with no state and no handlers has nothing for the browser to change. The build marks it `static` in the `--verbose` report: it has no browser twin, it is not in the island registry, it is not compiled and the server writes its markup with no island marker around it. The islands inside it are mounted by the document's own scan. That is what lets the recipes application's bundle be `src/**/*` and two generated files, with the client, its store, its Vue adapter and `vue` as its only externals:

```text
rendered  routes/layout.tsx#default          lowered     static
          routes/page.tsx#default            lowered     static
          routes/recipe/[id]/page.tsx#default lowered     static
```

The rule is the same in a React application. The storefront's catalog page and its layout are static too: the header the layout renders inline has state, so the renderer places it as an island of its own. A template hydrates only when it has state or handlers of its own. A page or a layout holding them is split at build time so that it stays static.

Navigation still keeps a static layout's DOM. A segment carries a digest of its own markup; a layout whose digest did not move is kept whatever changed below it. When it did move, after an action re-rendered it with a new count, the navigator replaces the layout's markup but keeps every island inside it that the new markup also places, by region key, moving the mounted element into the new markup and handing it its new props. The recipes masthead panel stays open through planning a recipe for that reason.

## The lab

Run `fsr build --verbose app` in the recipes example and read `app/generated/islands.ts`: three registrations, all `.vue`, one adapter loader. Read the report: every route module is `static` and the three components are `lowered` with `vue` in the detail column. View the source of a recipe page before the scripts run: the plan control is a `<sf-i>` holding the button Vue will hydrate, followed by its props. The three component stylesheets are linked in the head after `box.css`.

Take `snapfirec-vue` off `PATH` and build again. The bundle stops on the modules the registry imports and the report still prints, with a `plugins` row naming the binary and the install command. The three components are missing from `rendered` altogether rather than listed foreign, since nothing read them.

Give `Tonight.vue` a class in its `<script setup>`. `fsr build --verbose` keeps the page `static`, marks the component `foreign` with the line and the bundle compiles it as before: the panel mounts fresh and everything else on the page is as it was.

Put an unclosed tag in `Scaler.vue` and build. The plugin's diagnostic names the file and the line, the other two components compile and the build stops.

Give `routes/page.tsx` a `useState` from `react` and a button that sets it. Build: the state and the button move into `routes/page.island0.tsx`, an island beside the page. The page stays `static`. The island is React's, since its state comes from `react`. This application's import map has no React, so the build stops:

```text
`routes/page.island0.tsx#default` mounts through `@snapfire/fsr/client/react`, but the import map does not name `react` or `react-dom/client`; `fsr use <app dir> react` writes it
```

Remove the `useState` again and the application loads no React.

Open the masthead panel in a browser, then click "Cook this tonight" on a recipe. The count moves, the button changes and the panel is still open: the layout re-rendered around its island and the island kept its state.
