Tutorial 13 of 54 · for anyone who would rather write Svelte than React · Markdown
Write your islands in Svelte
You'll put a Svelte counter and a React counter on one page and make them share one number. Click either one and both move. The server renders both counters with the starting value, so the number is in the markup before any script runs.
Before you start
Install from crates.io. You need no Node and no package manager.
snapfirec doesn't compile Svelte itself. When it sees a .svelte file it hands it to snapfirec-svelte, which it looks for on your PATH. That's the third cargo install in the box above:
Svelte is vendored as it is published on npm, one file per module, with every subpath in the import map. Its subpaths share one runtime through relative imports, so they have to stay together in one tree under app/vendor/svelte/. clsx and esm-env are the two packages Svelte imports.
The build compiles your components with the compiler of the version you named. It fetches that compiler into app/.fsr/compilers/ the first time and never ships it. To move to another release later, run fsr use counter/app svelte@<version> again.
The wrote lines after it are left out here. The import map now names Svelte and React. The host adds both adapters, @snapfire/fsr/client/svelte and @snapfire/fsr/client/react, when it serves the page.
Give the app a port of its own in config/app.toml:
useStore from @snapfire/fsr/client/svelte gives you a Svelte store over the FSR key. You read it with $n and you write it with $n += 1, the way you use any Svelte store. Every other island that reads counter/clicks follows the write, whatever framework it's in.
start is the fallback. useStore(clicks, start) reads the key when something has set it and uses start when nothing has.
import{Island}from"@snapfire/fsr/template";importtype{RootProps}from"@generated/client";importSvelteCounterfrom"@src/ui/SvelteCounter.svelte";import{ReactCounter}from"@src/ui/ReactCounter";exportdefaultfunctionIndex({start}:RootProps){return(<sectionclassName="hero"><h1>Two counters, one number</h1><Islandwhen="load"><SvelteCounterstart={start}/></Island><Islandwhen="load"><ReactCounterstart={start}/></Island></section>);}
A .svelte file is imported by its default export. A component Svelte mounts can only be an island, so it has to sit inside Island.
Read the two island rows. Both say lowered, so the server renders both. The Svelte row says svelte in the last column: the build read the component through snapfirec-svelte and lowered it into the plan. The page itself is static, since it has no state of its own.
The build registers each island with its own framework's mounter:
app/generated/islands.ts
// Generated by fsr build. Do not edit.import{registerIsland}from"@snapfire/fsr/client";import{svelteMounter,sveltePatcher,svelteUnmounter,registerTree}from"@snapfire/fsr/client/svelte";import{reactMounter,reactPatcher,reactUnmounter,reactPauser}from"@snapfire/fsr/client/react";// @ts-ignore: the snippet Pieces.svelte exports from its module script, which the declaration of a `.svelte` file leaves outimport{tree}from"./components/Pieces.svelte";exportfunctionregisterIslands():void{registerTree(tree);registerIsland("src/ui/SvelteCounter.svelte#default",{loader:()=>import("../src/ui/SvelteCounter.svelte").then((m)=>m.default),mount:svelteMounter,patch:sveltePatcher,unmount:svelteUnmounter});registerIsland("src/ui/ReactCounter.tsx#ReactCounter",{loader:()=>import("../src/ui/ReactCounter.js").then((m)=>m.ReactCounter),mount:reactMounter,patch:reactPatcher,unmount:reactUnmounter,pause:reactPauser});}
Both counters show 3 before any script runs. The Svelte markup carries <!--[--> and <!--]-->, the hydration markers Svelte's own server renderer writes, so Svelte hydrates over it in the browser. The last line is the store seed from the loader's store export.
Open http://127.0.0.1:3401/ in a browser. Click the Svelte + twice and both counters read 5. Click the React - once and both read 4. Neither component imports the other. They meet only at the counter/clicks key.