Skip to content
Ilha
Esc
↑↓navigate↵open⌘Jpreview

Ilha for Svelte Developers

A practical map from Svelte $state, $derived, and $effect to ilha atoms and Effect derived atoms.

If you know Svelte, Ilha’s fine-grained reactivity should feel familiar. You write function components. You declare atom() values. Reads subscribe. Writes rerun the component and morph the DOM.

This is not an argument that Ilha is better than Svelte. It is a map from familiar Svelte ideas to Ilha.

What is Ilha?

Ilha is a small isomorphic UI library. A component renders HTML on the server and activates in the browser. You do not turn the whole page into a client app by default.

import { const atom: AtomFnatom } from "ilha";

export const const Counter: () => ViewCounter = () => {
  const const count: AtomHandle<number>count = atom<number>(init: number | Atom<number> | Effect<number, unknown, AtomRegistry> | Stream<number, unknown, AtomRegistry>, options?: AtomOptions<number> | undefined): AtomHandle<number>atom(0);
  return (
    <JSX.IntrinsicElements.button: ButtonHTMLAttributesbutton type?: string | undefinedtype="button" EventProps<HTMLButtonElement>.onclick?: ((event: Targeted<HTMLButtonElement, MouseEvent>) => void) | undefinedonclick={() => const count: AtomHandle<number>count.AtomHandle<number>.update: (f: (current: number) => number) => voidupdate((n: numbern) => n: numbern + 1)}>
      {const count: AtomHandle<number>count}
    </JSX.IntrinsicElements.button: ButtonHTMLAttributesbutton>
  );
};

$state vs atom()

Svelte reads and writes a variable. Ilha reads with count() and writes with .set() / .update(). In JSX, {count} subscribes.

import { const atom: AtomFnatom } from "ilha";

export const const Counter: () => ViewCounter = () => {
  const const count: AtomHandle<number>count = atom<number>(init: number | Atom<number> | Effect<number, unknown, AtomRegistry> | Stream<number, unknown, AtomRegistry>, options?: AtomOptions<number> | undefined): AtomHandle<number>atom(0);
  return (
    <JSX.IntrinsicElements.button: ButtonHTMLAttributesbutton type?: string | undefinedtype="button" EventProps<HTMLButtonElement>.onclick?: ((event: Targeted<HTMLButtonElement, MouseEvent>) => void) | undefinedonclick={() => const count: AtomHandle<number>count.AtomHandle<number>.update: (f: (current: number) => number) => voidupdate((n: numbern) => n: numbern + 1)}>
      {const count: AtomHandle<number>count}
    </JSX.IntrinsicElements.button: ButtonHTMLAttributesbutton>
  );
};

$derived vs Atom.map

import * as import AtomAtom from "effect/reactivity/Atom";
import { const atom: AtomFnatom } from "ilha";

export const const Totals: () => ViewTotals = () => {
  const const count: AtomHandle<number>count = atom<number>(init: number | Atom.Atom<number> | Effect<number, unknown, AtomRegistry> | Stream<number, unknown, AtomRegistry>, options?: AtomOptions<number> | undefined): AtomHandle<number>atom(0);
  const const doubled: AtomHandle<number>doubled = atom<number>(init: number | Atom.Atom<number> | Effect<number, unknown, AtomRegistry> | Stream<number, unknown, AtomRegistry>, options?: AtomOptions<number> | undefined): AtomHandle<number>atom(import AtomAtom.const map: <Atom.Atom<number>, number>(self: Atom.Atom<number>, f: (_: number) => number) => Atom.Atom<number> (+1 overload)
Maps the current value of an atom with a pure function. **Details** When the source atom is writable, the returned atom remains writable and keeps the source atom's write input type.
@stabilityunstable@categorycombinators@since4.0.0
map
(const count: AtomHandle<number>count.AtomHandle<number>.atom: Atom.Atom<number>atom, (n: numbern) => n: numbern * 2));
return <JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>{const doubled: AtomHandle<number>doubled}</JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>; };

Async and streamed data use when / Stream views, not a derived accessor with .loading.

$effect vs watch()

import { const atom: AtomFnatom, const watch: WatchFnwatch } from "ilha";

const const Title: () => ViewTitle = () => {
  const const title: AtomHandle<string>title = atom<string>(init: string | Atom<string> | Effect<string, unknown, AtomRegistry> | Stream<string, unknown, AtomRegistry>, options?: AtomOptions<string> | undefined): AtomHandle<string>atom("Hello");
  watch<string>(source: Atom<string> | AtomHandle<string> | Stream<string, unknown, unknown>, fn: WatchCallback<string>): Instruction<undefined>watch(const title: AtomHandle<string>title, (value: stringvalue) => {
    var document: Document
**`window.document`** returns a reference to the document contained in the window. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/document)
document
.Document.title: string
The **`document.title`** property gets or sets the current title of the document. When present, it defaults to the value of the <title>. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/title)
title
= value: stringvalue;
}); return <JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>{const title: AtomHandle<string>title}</JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>; };

Mount-only work uses watch.once() — return a cleanup for unmount. For stream-driven ongoing logic, use when with Atom.toStream — see Streams. For write-site DOM effects, handlers are fine.

Props

Props are ordinary function arguments. There is no $props() rune.

Boundaries

A nested function shares the parent component. mount / renderToString on a function makes it a root.

SSR

await renderToString(Counter) for HTML. mount(host, Counter, { hydrate: true }) to attach events.

Svelte Ilha
$state atom(value)
$derived atom(Atom.map(...)) or atom(Atom.transform(...))
$effect watch() / watch.once() or event handlers
setContext createContext / context()
+error ErrorBoundary (island) / router +error (route)
bind:value oninput + atom
.svelte component function component
Written by awskir

Was this page helpful?