← Back to list

We’ve had reactivity on the browser since 2018, with async iterables

And it can be used to power client-side rendering

Thomas Juster in Level Up Coding · 2026-03-22 08:49 · 50 claps · 2.7 min read
#frontend #reactivity #jsx
Open on Medium ↗
Wiki topics: 🌐 · Web Development

We’ve had reactivity on the browser since 2018, with async iterables

This week I had an epiphany, we already have a native reactivity primitive in JavaScript: Async Iterables.

The model is surprisingly simple:

  • first yield = initial state
  • subsequent yields = updates
  • for await (…) = subscription

That’s it — a read-only state is just an AsyncIterable.

The implications are of this approach are wide: anything supporting the Async Iterator protocol — even Web Standards like Server-Sent Events or Web Sockets — can directly be treated as a state.

This got me thinking about how every frontend framework reinvents reactivity:

  • React → class this.state then useState hook
  • Vue → refs/reactivity
  • Angular → RxJS → signals
  • Svelte → stores → runes
  • Solid → signals

To be fair, most of them were written before 2018. But what if we didn’t need any of that?

I built an experiment called **Yawn** to explore this idea:

  • uses Async Iterables as the only reactivity primitive
  • API mirrors Web standards (HTML attributes, DOM lifecycle, etc.)
  • JSX returns real DOM nodes
  • ~2.6kB rendered, ~1.5kB gzipped — without even trying to optimize it yet

Repo + demo: https://github.com/SacDeNoeuds/yawn

The goal is to ask: Could a frontend library be “forever v1” by aligning with the Web platform?

Me, after 10 years of JavaScript, when yet a new framework is created

Me, after 10 years of JavaScript, when yet a new framework is created

Usage

Counter example — showcasing a writable state

Let’s start with a good ol’ counter example to explore writable states:

import { State } from '@sacdenoeuds/yawn'

export function Counter() {
  const count = new State(0)
  const decrement = () => count.update((count) => count - 1);
  const increment = () => count.update((count) => count + 1);
  const reset = () => count.set(0);

  return (
    <div class="counter" data-count={count}>
      <button type="button" onclick={decrement}>
        -
      </button>

      <span>{count}</span>

      <button type="button" onclick={increment}>
        +
      </button>
    </div>
  );
}

Doubling the count — showcasing a derived state

The count state can be derived using an async iterable mapper:

type Props = { count: AsyncIterable<number> }

function Doubled({ count }: Props) {
  // …
  const doubled = count.map((count) => count * 2)
  return <div>{doubled}</div>
}

Fetching a todo — showcasing a read-only custom state

We can also create read-only states from a simple async generator, let’s illustrate this with a todo fetcher:

type Todo { id: number, title: string, completed: boolean }

async function* makeTodoFetcher(todoId: number) {
  yield { status: 'pending' } as const
  //  ^-> emit a state update

  try {
    const response = await fetch(`…/todo/${todoId}`)
    if (!response.ok) throw response
    const todo: Todo = await response.json()

    yield { status: 'ok', todo } as const
    //  ^-> emit a state update

  } catch (error) {
    yield { status: 'failed', error } as const
    //  ^-> emit a state update
  }
}

Now we can call the makeTodoFetcher to create a todo fetch state:

type Props = { todoId: number }

export function TodoFetcher({ todoId }: Props) {
  const fetchState = makeTodoFetcher(todoId)

  return fetchState.map((fetchState) => {
    switch (fetchState.status) {
      case 'pending':
        return <div>Loading…</div>
      case 'failed':
        return <div>Error: {String(fetchState.error)}</div>
      case 'ok':
        return <Todo todo={fetchState.todo} />
    }
  })
}

Closing word

There are still open questions (memory, ergonomics), but the model feels surprisingly coherent once it clicks.

I am curious to hear thoughts — especially from people who’ve worked with signals, RxJS, or async iterators in production.

I use the GitHub stars as an interest-meter, so if you’d like to see this pushed further, please star the project.

Thanks for reading 💛


메타데이터
post_id
e4f0d6487a95
slug
weve-had-reactivity-on-the-browser-since-2018-with-async-iterables-e4f0d6487a95
url
https://levelup.gitconnected.com/weve-had-reactivity-on-the-browser-since-2018-with-async-iterables-e4f0d6487a95
canonical_url
https://levelup.gitconnected.com/weve-had-reactivity-on-the-browser-since-2018-with-async-iterables-e4f0d6487a95
author_url
https://medium.com/@thomas-juster
status
ok
fetched_at
2026-06-11 17:15:47