---
title: Core concepts
description: 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`      |

```tsx twoslash
import { atom } from "ilha";

const Badge = (props: Record<string, unknown>) => (
  <span>{String(props.label)}</span>
);

const Label = (props: Record<string, unknown>) => {
  const text = atom(String(props.label));
  return <Badge label={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.

```tsx twoslash
import { atom } from "ilha";

const Counter = () => {
  const count = atom(0);
  count(); // read
  count.set(5); // write
  count.update((n) => n + 1);
  return <p>{count}</p>;
};
```

Derived values use Effect's `Atom.map` or `Atom.transform`:

```tsx twoslash
import * as Atom from "effect/reactivity/Atom";
import { atom } from "ilha";

const Cart = () => {
  const items = atom([{ n: 1 }, { n: 2 }]);
  const total = atom(
    Atom.map(items.atom, (list) => list.reduce((sum, item) => sum + item.n, 0))
  );
  return <p>{total}</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()`](/guide/ui/state#side-effects) for side effects on atom changes.

## JSX

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

```tsx twoslash
import { atom } from "ilha";

const App = () => {
  const open = atom(false);
  return (
    <button type="button" onclick={() => open.set(true)}>
      Open
    </button>
  );
};
```

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.
