# Introduction Route: /guide/getting-started/introduction Source: /guide/getting-started/introduction/index.md import { Preview } from "$lib/components/preview"; export const example = `import ilha from "ilha"; import { Button } from "areia"; export default ilha .state("count", 0) .on("button@click", ({ state }) => state.count(state.count() + 1)) .render(({ state }) => (
Count: {state.count()}
Count: {state.count()}
); ` # State Ilha uses Signals to make your components reactive. Use the `.state()` builder method to define state - any property declared here will automatically trigger UI updates when it changes. Pass the property name and its initial value. All state properties are then available inside `.render()` via the `state` property.Count: {state.count()}
Count: {state.count()}
Count: {state.count()}
Count: {state.count()}
Count: {state.count()}
> )); ` # On Use the `.on()` method to attach event listeners to elements inside your component. It takes a selector and an event name joined by `@`, and a callback that receives the component context — giving you direct access to `state` and more. ```ts .on("[selector]@eventName", callback) ``` The selector works like `document.querySelector` scoped to your component's rendered output. This means you can target any attribute, class, or element — `[data-action=increase]`, `.btn-submit`, `input` — without worrying about conflicts with the rest of the page. Inside the callback, updating state is as simple as calling the state property as a function with a new value. Ilha will re-render only what changed — so clicking the button below updates the count without touching anything else in the DOM.{state.count()}
``` This keeps reactive state small and direct. You read what you need, update what you need, and the island updates accordingly. ## Builder chain You create islands with a fluent builder chain. Each method adds one capability, and [`.render()`](/guide/island/render) finalizes the component. A typical island might include: - [`.input()`](/guide/island/input) for typed props. - [`.state()`](/guide/island/state) for local reactive state. - [`.derived()`](/guide/island/derived) for computed or async values. - [`.on()`](/guide/island/on) for event handlers. - [`.effect()`](/guide/island/effect) and [`.onMount()`](/guide/island/onmount) for side effects. - [`.css()`](/guide/island/css) for scoped styles. - [`.render()`](/guide/island/render) to produce the final island. This step-by-step structure is one of the core design ideas in ilha. Instead of putting everything in one large options object, you compose behavior in a readable chain. ## JSX rendering ilha can render islands with JSX. Configure TypeScript with `jsxImportSource: "ilha"`, then return JSX from [`.render()`](/guide/island/render). ```tsx twoslash const userInput = "Ilha is awesome"; // ---cut--- import ilha from "ilha"; const Message = ilha.render(() =>{userInput}
); ``` JSX output follows safe rendering rules: interpolated values are escaped by default, arrays render without commas, and ilha values such as child islands can be nested directly. If you really need to inject trusted markup, you can opt into that explicitly with [`raw()`](/guide/helpers/raw). ## Derived values Not every value belongs in local state. Sometimes a component needs data that depends on state or input, including async data. That is what [`.derived()`](/guide/island/derived) is for. Each derived entry is a signal accessor — read it with `derived.name()`, the same way you read `state.count()`. You can also write `derived.name(value)` for optimistic UI. For async work, the accessor also exposes `loading`, `value`, and `error`, so loading and error states stay part of the normal rendering model instead of something bolted on from the outside. ## Events and effects ilha separates user interaction from side effects. Use [`.on()`](/guide/island/on) for DOM events such as clicks, input, and change events. Use [`.effect()`](/guide/island/effect) when you want reactive behavior that runs after mount and reruns when its dependencies change. Use [`.onMount()`](/guide/island/onmount) when something should run once after the island is attached to the DOM. This separation helps keep component logic easier to scan: - Events respond to user actions. - Effects respond to reactive changes. - Mount hooks handle lifecycle setup. ## Scoped styles ilha supports component-level styles with [`.css()`](/guide/island/css). Styles are scoped to the island so they stay local and do not leak into nested child islands. This lets you keep structure, behavior, and styling close together when that is useful, without giving up isolation. ## SSR and hydration ilha is designed to work naturally with server rendering and hydration. You can render HTML on the server, send it to the browser, and later activate the island in place. When using hydratable output, ilha can also embed snapshots of state and derived values. That helps restore the component without unnecessary work on first mount. ## Mental model A useful way to think about an island is: - **Input** is data coming in. - **State** is reactive data owned by the component. - **Derived** is data computed from input or state. - **Render** turns all of that into HTML. - **Mount** activates behavior in the browser. If you keep that model in mind, most of the API becomes intuitive. Each builder method just adds one more piece to that flow. --- # .derived() Route: /tutorial/counter/derived Source: /tutorial/counter/derived/index.md import { Preview } from "$lib/components/preview"; export const example = `import ilha from "ilha"; import { Button } from "areia"; export default ilha .state("count", 1) .derived("doubled", ({ state }) => state.count() * 2) .on("[data-action=increase]@click", ({ state }) => { state.count(state.count() + 1); }) .render(({ state, derived }) => ( <>Count: {state.count()}
Doubled: {derived.doubled()}
> )); ` # Derived Now that you can mutate state with `.on()`, derived properties let you compute values from that state automatically — recalculating only when their dependencies change. Use `.derived()` to keep complex logic out of your templates and make components easier to reason about. ```ts .derived("name", ({ state }) => /* computed value */) ``` The callback receives the component context, just like `.on()`. The result is available in `.render()` via `derived.name()` — the same call syntax as state. You can also write `derived.name(value)` for optimistic UI; the next time the derived function runs, it overwrites with the computed result. `.derived()` also accepts an async function, making it suitable for data fetching. Async derived values expose `loading`, `value`, and `error` on the accessor — so you get built-in async state without reaching for SWR or TanStack Query.Count: {state.count()}
Doubled: {derived.doubled()}
> )); ` # Bind Use the `bind:` template syntax in JSX to create a two-way connection between a state signal and a form element. When the state changes, the input updates. When the user types, the state updates — no event listener boilerplate required. ```ts ; ``` The `bind:` prefix goes on the attribute, and the value is a signal accessor — either a local `.state()` accessor or an external `signal()`. Ilha infers the correct value property automatically: `value` for text and number inputs, `checked` for checkboxes. Because binding is declared directly in the template, it lives right next to the element it controls. In the example above, the input and the button both control the same `count` state — either can update it, and both stay in sync.Count: {state.count()}
Doubled: {derived.doubled()}
> )); ` # Effect Use the `.effect()` method to run a side effect whenever reactive state changes. The callback receives the component context — giving you access to `state`, `derived`, and more — and runs automatically after every state update. ```ts .effect(({ state, derived }) => { ... }) ``` Effects are the right place for logic that needs to _react_ to state but doesn't belong in an event handler. This includes enforcing constraints, syncing to external systems, triggering animations, or logging. In the example below, the effect watches `count` and resets it to `0` the moment it exceeds `3`. Because the reset itself is a state update, Ilha re-runs the effect to confirm the new value satisfies the condition — so your constraints are always guaranteed. Effects run synchronously after each state change. If you need to interact with the DOM after a render — for example, to measure an element or focus an input — use `queueMicrotask()` or `requestAnimationFrame()` inside the callback to defer execution until the render is complete.Count: {state.count()}
Doubled: {derived.doubled()}
> )); ` # On Mount Use .onMount() to run logic once, immediately after the component renders for the first time. Unlike .effect(), it does not re-run on state changes — making it the right place for one-time setup work. The callback receives the full component context, so you have direct access to state and derived. This makes .onMount() ideal for seeding initial data: fetch a resource, resolve its result, and write it into state — Ilha will re-render automatically once the data arrives.Hello, {input.name ?? "World"}!
)); `; # Input Declares the island's external props and their types. Two forms are supported: a type-only generic for when you just need TypeScript inference, and a [Standard Schema](https://standardschema.dev/)-compatible validator (Zod, Valibot, ArkType, etc.) for runtime validation too. ## Basic usage **Type-only** — TypeScript inference, no runtime validation:Hello, {input.name}!
); Greeting(); // →Hello, World!
Greeting({ name: "ilha" }); // →Hello, ilha!
``` ```tsx twoslash import ilha from "ilha"; ilha .input<{ name: string }>({ name: "World" }) .render(({ input }) =>{input.name}
); ``` **With a schema** — inference plus runtime validation and coercion: ```tsx twoslash import ilha from "ilha"; import { z } from "zod"; const Greeting = ilha .input(z.object({ name: z.string().default("World") })) // [!code highlight] .render(({ input }) =>Hello, {input.name}!
); Greeting.toString({ name: "ilha" }); // →Hello, ilha!
Greeting.toString(); // →Hello, World!
``` ## Why use `.input()` Without `.input()`, any props passed to an island are untyped and unvalidated. Adding a type or schema gives you: - Full TypeScript inference for `input` inside [`.state()`](/guide/island/state), [`.render()`](/guide/island/render), [`.on()`](/guide/island/on), [`.effect()`](/guide/island/effect), and every other builder method. - Runtime validation and coercion on every call, including during SSR and hydration (schema form only). - Default values handled by the schema itself, so the island works without props (schema form only). ## Choosing a form | | `.input{state.count()}
); ``` The initializer function receives the resolved input object, so state stays in sync with whatever props were passed in. This works identically with both forms. ## Async schemas Async schemas are not supported. If your validator's `validate()` method returns a `Promise`, ilha will throw at runtime. Keep schemas synchronous. ## Notes - `.input()` must be called before any other builder method if you want the input type to flow through the chain. - Calling `.input()` resets the builder — any previously chained [`.state()`](/guide/island/state) or other methods are not carried over. - If `.input()` is omitted entirely, props are accepted as `RecordCount: {state.count()}
{state.count()}
); ``` This is evaluated once at mount time. The initializer is not reactive — it only runs when the island is first created. ## Multiple state entries Chain `.state()` as many times as needed. Each key becomes a typed accessor on the `state` object: ```tsx twoslash import ilha from "ilha"; const Form = ilha .state("name", "") .state("submitted", false) .state("count", 0) .render(({ state }) => ({state.name()} — {state.count()}
)); ``` ## Inside JSX Signal accessors can be rendered directly in JSX without calling them. ilha detects signal accessors and calls them automatically, and applies HTML escaping: ```tsx twoslash import ilha from "ilha"; const Island = ilha .state("label", "hello") .render(({ state }) =>{state.label}
); ``` If you call `state.label()` explicitly it works the same way — both forms are equivalent inside JSX. ## Updating state from events State accessors are plain functions, so they work directly as setters inside event handlers: ```tsx twoslash import ilha from "ilha"; const Toggle = ilha .state("open", false) .on("button@click", ({ state }) => state.open(!state.open())) // [!code highlight] .render(({ state }) => (Content
: ""}Total: {derived.total()}
Total: {derived.total()}
); ``` The next time the derived function runs (for example after state changes or an async fetch resolves), it overwrites the optimistic value with the computed result. ## The derived envelope Every derived value also exposes `loading`, `value`, and `error` on the accessor itself. Use these when you need explicit control — especially with async derived values: | Property | Type | Description | | --------- | -------------------- | ------------------------------------- | | `loading` | `boolean` | `true` while the function is running | | `value` | `T \| undefined` | The last successfully resolved value | | `error` | `Error \| undefined` | Set if the function threw or rejected | ```ts derived.total(); // same as derived.total.value when resolved derived.total.value; // envelope read derived.total.loading; // false for sync derived after first run derived.total.error; // undefined when no error ``` For synchronous derived values, `loading` is `false` after the first run and `()` returns the computed value directly. For async derived values, check `loading` and `error` before reading `value` or calling `()`. ## Async derived values Pass an async function to fetch data or run any other asynchronous work. The envelope tracks progress while the promise is pending: ```tsx twoslash import ilha from "ilha"; const UserCard = ilha .state("userId", 1) // [!code highlight:4] .derived("user", async ({ state, signal }) => { const res = await fetch(`/api/users/${state.userId()}`, { signal, }); return res.json(); }) .render(({ derived }) => { if (derived.user.loading) returnLoading…
; if (derived.user.error) returnError: {derived.user.error.message}
; return{derived.user().name}
; }); ``` On first render, `loading` is `true` and `derived.user()` is `undefined` until the promise resolves. ## Reactive dependencies The derived function re-runs whenever any signal it reads changes. Dependencies are tracked automatically — you do not need to declare them manually. ```tsx twoslash import ilha from "ilha"; const Search = ilha .state("query", "") .derived("results", async ({ state, signal }) => { const res = await fetch(`/api/search?q=${state.query()}`, { signal, }); return res.json() as PromiseSearching…
) : ({derived.data()?.name ?? "…"}
)); ``` If the signal was already aborted before your async work completes, the result is discarded silently. ## Keeping stale value during reload When a derived function re-runs, `loading` becomes `true` but `value` retains the previous result until the new one resolves. This lets you avoid layout shifts by showing stale content while refreshing: ```tsx twoslash import ilha from "ilha"; const Island = ilha .state("page", 1) .derived("items", async ({ state, signal }) => { const res = await fetch(`/api/items?page=${state.page()}`, { signal, }); return res.json() as Promise