---
title: Streams
description: Paint Effect Streams of views, bridge atoms with Atom.toStream, and use when for per-emission generator bodies.
---

## Streams

Generator components can **yield** a `Stream` of views. Compose with Effect (`Stream.map`, `Stream.pipe`, …) — Ilha repaints on each emission. SSR takes the first value only; the client keeps listening after `mount`.

```tsx twoslash
import * as Stream from "effect/Stream";

function* Status() {
  yield Stream.map(
    Stream.fromIterable(["idle", "ready"]),
    (value) => <p>{value}</p>,
  );
}
```

Async components can return a `Stream` directly — no generator required.

## Atom.toStream

Turn an atom into a change feed with [`Atom.toStream`](https://www.effect.website/docs/v4/api/effect/unstable/reactivity/Atom#tostream):

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

function* List() {
  const items = atom(["a", "b"]);
  yield Stream.map(Atom.toStream(items.atom), (list) => (
    <ul>
      {list.map((item) => (
        <li key={item}>{item}</li>
      ))}
    </ul>
  ));
}
```

Use `items().map(...)` in a sync component for a simple list. Reach for `Atom.toStream` when a stream pipeline should drive updates (debounce, merge, server feeds).

## when

`when(stream, body)` runs a generator `body` per emission. A new value interrupts the previous body — stale async work does not paint.

```tsx twoslash
import * as Effect from "effect/Effect";
import * as Stream from "effect/Stream";
import * as Atom from "effect/unstable/reactivity/Atom";
import { atom, when } from "ilha";

function* Search() {
  const query = atom("");
  yield (
    <input
      value={query}
      oninput={(e: Event) =>
        query.set((e.currentTarget as HTMLInputElement).value)
      }
    />
  );
  yield* when(
    Atom.toStream(query.atom).pipe(
      Stream.debounce("200 millis"),
    ),
    function* (q) {
      if (!q) {
        yield <p>Type to search</p>;
        return;
      }
      const items = yield* Effect.tryPromise({
        try: (signal) =>
          fetch(`/api/search?q=${encodeURIComponent(q)}`, {
            signal,
          }).then((r) => r.json()),
        catch: (e) => e,
      });
      yield (
        <ul>
          {(items as string[]).map((item) => (
            <li key={item}>{item}</li>
          ))}
        </ul>
      );
    },
  );
}
```

`yield Stream.map(...)` is enough for a stream of views. Use `when` when each emission needs its own async generator.

## Generators

| Yield              | Meaning                                    |
| ------------------ | ------------------------------------------ |
| `yield <p>…</p>`   | Paint this view                            |
| `yield stream`     | Subscribe to a `Stream` of views           |
| `yield* effect`    | Run an `Effect` (fetch, sleep, `Deferred`) |
| `yield* when(...)` | Per-emission body; stale bodies interrupt  |

Uncaught failures paint an error view.

## Side effects

For atom-driven side effects in any component, use [`watch()`](/guide/ui/state#side-effects). In generators, `yield* Stream.runForEach` works for finite streams:

```tsx twoslash
import * as Effect from "effect/Effect";
import * as Stream from "effect/Stream";

function* Boot() {
  yield* Stream.runForEach(
    Stream.fromIterable([1, 2, 3]),
    (n) => Effect.sync(() => console.log(n)),
  );
  yield <p>Ready</p>;
}
```

Co-locate DOM effects in event handlers when you own the write site.

## Deferred

Paint UI, wait for input, then continue:

```tsx twoslash
import * as Deferred from "effect/Deferred";
import * as Effect from "effect/Effect";

function* Gate() {
  const deferred = yield* Deferred.make<string>();
  yield (
    <button
      type="button"
      onclick={() =>
        Effect.runSync(Deferred.succeed(deferred, "Ada"))
      }
    >
      Continue
    </button>
  );
  const name = yield* Deferred.await(deferred);
  yield <p>Hello, {name}</p>;
}
```

## Server feeds

Map a server stream to JSX with `Stream.fromAsyncIterable`. SSR serializes the first value; the client resumes the feed. See [Server islands](/guide/routing/server-islands) and [PubSub state](/guide/recipes/pubsub-state).
