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

Core concepts

components, atoms, JSX, mount, and renderToString.

ilha is built around a small set of ideas: components, atoms, JSX, and isomorphic render. Once these click, the rest of the library feels straightforward.

Component

A component is a function that returns a view. It can be sync, async, or a generator.

The same component can render to HTML and mount in the browser. It owns the atoms you declare while it runs.

A nested plain function belongs to the parent component. Its atom() calls share that parent.

Choose the smallest form

Form Ownership
const View = () => JSX Parent component owns atoms and cleanup
mount(el, View) View is the root fiber
function* / async Same root, with yield* / await
import { const atom: AtomFnatom } from "ilha";

const const Badge: (props: Record<string, unknown>) => ViewBadge = (props: Record<string, unknown>props: type Record<K extends keyof any, T> = { [P in K]: T; }
Construct a type with a set of properties K of type T
Record
<string, unknown>) => (
<JSX.IntrinsicElements.span: HTMLAttributes<HTMLSpanElement>span>{
var String: StringConstructor
(value?: any) => string
Allows manipulation and formatting of text strings and determination and location of substrings within strings.
String
(props: Record<string, unknown>props.unknownlabel)}</JSX.IntrinsicElements.span: HTMLAttributes<HTMLSpanElement>span>
); const const Label: (props: Record<string, unknown>) => ViewLabel = (props: Record<string, unknown>props: type Record<K extends keyof any, T> = { [P in K]: T; }
Construct a type with a set of properties K of type T
Record
<string, unknown>) => {
const const text: AtomHandle<string>text = atom<string>(init: string | Atom<string> | Effect<string, unknown, AtomRegistry> | Stream<string, unknown, AtomRegistry>, options?: AtomOptions<string> | undefined): AtomHandle<string>atom(
var String: StringConstructor
(value?: any) => string
Allows manipulation and formatting of text strings and determination and location of substrings within strings.
String
(props: Record<string, unknown>props.unknownlabel));
return <const Badge: (props: Record<string, unknown>) => ViewBadge label: stringlabel={
const text: AtomHandle
() => string
text
()} />;
};

Read an atom with text() when you need the current value in JS. In JSX, {text} subscribes the render.

Isomorphic components

Use the same function two ways:

  • await renderToString(component) for HTML.
  • mount(element, component) for the browser.

You do not split a component into server and client copies.

Atoms

An atom is a value you can read and update. When it changes, the component that read it reruns.

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

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);
  
const count: AtomHandle
() => number
count
(); // read
const count: AtomHandle<number>count.AtomHandle<number>.set: (next: number) => voidset(5); // write const count: AtomHandle<number>count.AtomHandle<number>.update: (f: (current: number) => number) => voidupdate((n: numbern) => n: numbern + 1); return <JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>{const count: AtomHandle<number>count}</JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>; };

Derived values use Effect’s Atom.map or Atom.transform:

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

const const Cart: () => ViewCart = () => {
  const 
const items: AtomHandle<{
    n: number;
}[]>
items
=
atom<{
    n: number;
}[]>(init: {
    n: number;
}[] | Atom.Atom<{
    n: number;
}[]> | Effect<{
    n: number;
}[], unknown, AtomRegistry> | Stream<{
    n: number;
}[], unknown, AtomRegistry>, options?: AtomOptions<{
    n: number;
}[]> | undefined): AtomHandle<{
    n: number;
}[]>
atom
([{ n: numbern: 1 }, { n: numbern: 2 }]);
const const total: AtomHandle<number>total = 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<{
    n: number;
}[]>, number>(self: Atom.Atom<{
    n: number;
}[]>, f: (_: {
    n: 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 items: AtomHandle<{
    n: number;
}[]>
items
.
AtomHandle<{ n: number; }[]>.atom: Atom.Atom<{
    n: number;
}[]>
atom
, (
list: {
    n: number;
}[]
list
) =>
list: {
    n: number;
}[]
list
.
Array<{ n: number; }>.reduce<number>(callbackfn: (previousValue: number, currentValue: {
    n: number;
}, currentIndex: number, array: {
    n: number;
}[]) => number, initialValue: number): number (+2 overloads)
Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.
@paramcallbackfn A function that accepts up to four arguments. The reduce method calls the callbackfn function one time for each element in the array.@paraminitialValue If initialValue is specified, it is used as the initial value to start the accumulation. The first call to the callbackfn function provides this value as an argument instead of an array value.
reduce
((sum: numbersum,
item: {
    n: number;
}
item
) => sum: numbersum +
item: {
    n: number;
}
item
.n: numbern, 0))
); return <JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>{const total: AtomHandle<number>total}</JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>; };

Do not put JSX in an atom. Atoms hold data. Map arrays during render with items().map(...), or paint a Stream for live server data. Use watch() for side effects on atom changes.

JSX

Prefer JSX. Interpolated values escape. Event props are lowercase (onclick, onchange).

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

const const App: () => ViewApp = () => {
  const const open: AtomHandle<boolean>open = atom<boolean>(init: boolean | Atom<boolean> | Effect<boolean, unknown, AtomRegistry> | Stream<boolean, unknown, AtomRegistry>, options?: AtomOptions<boolean> | undefined): AtomHandle<boolean>atom(false);
  return (
    <JSX.IntrinsicElements.button: ButtonHTMLAttributesbutton type?: string | undefinedtype="button" EventProps<HTMLButtonElement>.onclick?: ((event: Targeted<HTMLButtonElement, MouseEvent>) => void) | undefinedonclick={() => const open: AtomHandle<boolean>open.AtomHandle<boolean>.set: (next: boolean) => voidset(true)}>
      Open
    </JSX.IntrinsicElements.button: ButtonHTMLAttributesbutton>
  );
};

Use a plain function for DOM events. You do not need a special action wrapper on the client.

SSR and hydration

renderToString(component) paints into a DOM, waits until idle, then returns HTML. By default it wraps the output in <div data-ilha data-ilha-state="…">.

mount(host, component, { hydrate: true }) restores atom snapshots from that host and attaches events.

Mental model

  1. A component runs and returns a view.
  2. Atoms it reads subscribe that run.
  3. A write schedules a rerun and morphs the DOM.
  4. Streams and generators paint into holes inside the host.

Was this page helpful?