自2018年以来,我们已经通过Async Iterables实现了前端响应式。
We've had front end reactivity since 2018 – via Async Iterables

原始链接: https://github.com/SacDeNoeuds/yawn

## Yawn:一个“永远v1”前端库 一个最近的认识促成了Yawn的想法:前端响应式自2018年以来就已存在,通过Async Iterables实现!与其不断学习新的框架特定响应式系统(useState、refs、Signals等),Yawn利用了这一内置的Web平台特性。这旨在对抗“JavaScript疲劳”——无休止地采用和重新学习前端工具的循环。 Yawn 提出了一个最小的API(约2个函数,1个类),镜像Web标准,承诺一个不会改变的库。它利用标准的HTML属性和事件监听器,响应式由Async Iterables驱动。核心组件是用于管理响应式数据的`State`类。 该项目目前是一个概念验证 (),拥有极小的体积(2.6kB gzip压缩后),并提供诸如延迟求值、与现有迭代器辅助工具的兼容性以及与后端流直接集成的潜力等优势。 作者寻求社区支持——如果GitHub仓库达到5,000颗星,则目标是向生产就绪推进。目标是构建一个基于持久Web基础的库,提供稳定且熟悉的发展体验。

对不起。
相关文章

原文

Yesterday night (18th of March, 2026), I had an epiphany: we have reactivity in the frontend. Since 2018 no less!

In the name of … Async Iterables 🎉

How it works:

  • the first value of the async iterable is its initial value at the time of for await (…) declaration
  • all the next values are state updates
  • the async iterable awaits each next update.

That's it.

A read-only state is basically an AsyncIterable, nothing more.

To take an example, this is a valid state:

type TodoId = number
async function* fetchTodo(todoId: TodoId) {
  yield { status: 'pending' } as const
  try {
    const response = await fetch(`…/todo/${todoId}`)
    const todo = await response.json()
    yield { status: 'success', todo } as const
  } catch (error) {
    yield { status: 'failure', error } as const
  }
}
const todo = fetchTodo(1)
//    ^-> this is a valid state.

Another example of a todo fetcher based on a todo id in route parameters:

async function* fetchPageTodo(todoIds: AsyncIterable<TodoId>) {
  for await (const todoId of todoIds) {
    yield* fetchTodo(todoId)
  }
}
const todoOfPage = fetchPageTodo(route.params.todoId)
//    ^-> this is a valid state.

State composition can be achieved by composing native AsyncIterable using soon-to-come iterator helpers, which may involve a learning curve but once you climbed it, you earned JS knowledge that will remain forever 💛.

Okay, then why making such a fuss?

Because reactivity is what powers client-side rendering libraries, all of them. Rendering a simple div is easy, but how do you re-render when your state changes?

Any client-side rendering library needs to build their own reactivity system:

  • React ships its useState
  • Vue goes with ref & co
  • Angular delegated it to RxJS
  • Svelte had observables-like stores
  • Solid made its case about Signals and then Svelte got runes, and Angular moved to Signals too.

Although there is a TC39 proposal for Signal, no consensus yet.

Now again, we have reactivity in the frontend, since 2018.

Which got me thinking: I started my career in 2016 and in 10 years using frontend frameworks, we repeatedly needed to learn new tools, APIs, syntaxes, etc. inducing that famous JavaScript Fatigue.

Which contrasts with the fact that the Web itself is never introducing breaking changes, and that is its core philosophy.

Looking back, those frameworks trapped developers in the loop of continuously needing to learn tools that will disappear or change in the next ~5 years, here are a few critics.

Note

The critics I am about to share are not a diatribe, all these tools had to re-implement their version of everything depending on the state (🤭) of the Web Platform at the time of their creation, and their choices pushed the Ecma committee to add features to the Web, so a big thanks is in order.

Now that the mindset is clarified, let's go back to those critics – and those are just the tip of the iceberg:

  • React went from classes to hooks, then added server-side apps, and decided to name the input event listener onChange instead of onInput which is the real standard listener (very few React developers actually know that, by the way). It took years to support custom elements. The list is really long when it comes to React.
  • Vue had the Option API and then added the Composition API to support hooks in disguise composables, introducing @vue/reactivity at the time. It reuses Web APIs names in non-compatible ways (hello Slot props, templates).
  • Angular moved to RxJS in v2, now moved to Signals.
  • Svelte moved from observable-ish stores to runes, and same as Vue it reuses Web APIs names in non-compatible ways (hello Slot props).
  • As much as I like SolidJS (my favorite so far), it adds directives, stores, and with an error-prone quirk of requiring to NOT destructure component props.

I could go on for a very long while, to be honest.

Now remember, any client-side rendering library is coupled to its reactivity system. Therefore, we cannot just say "Hey, change your reactivity system and you're good to go". But what if…

What if we had that client-side library that tackles two problems at once?

  • the need to learn a new reactivity system that will get obsolete in ~5 years
  • the need to learn a new client-side rendering library that will get obsolete in ~5 years

What if we had a library embracing the Web's philosophy, by offering a super-small API surface on top of Web APIs (~2 functions and 1 class) and providing an API which does not change by design because it is mirrored on Web APIs?

The mantra being: You know Web Standards & APIs ↔︎ You know the library's API.

The promise of a forever v1 library 💛

Here comes Yawn, the forever v1 library embryo

The library is mirrored on Web Standards & APIs to capitalize on your knowledge and embrace JavaScript fatigue.

  • JSX elements return HTML elements: const element: HTMLDivElement = <div />
  • The mark-up is mirrored on HTML standard:
    • event listeners are lowercase: <div onclick={…} />
    • attributes are the same as HTML's: <label class="some class" for="some-field-id" />
      • no className attribute, directly class
      • no htmlFor attribute, directly for
    • The only addition is the – debatable – special ref attribute to access an element straight after its creation
  • No onMount, useEffect or any special concept, as mentioned by MDN elements are "connected" and "disconnected" from the DOM
  • Reactivity is powered by AsyncIterable – see more examples below
  • As for CSS, I suggest you use the excellent scope at-rule (MDN) and stick to simple .css stylesheets

Because of this approach, the library is super slim: 2.6kB rendered !

If you are interested in pushing this further, please star the project. If the repo reaches 5,000+ stars I will start making it production-ready and look for contributors.

You can also open issues to start discussions.

Proof of Concept – a glimpse of the API

The demo is located at https://github.com/SacDeNoeuds/yawn/tree/main/demo and deployed at https://sacdenoeuds.github.io/yawn/

Run it locally by cloning the project and run npm run demo.

Example of a component with reactive state

To be writable, Yawn exposes a State class providing 2 additional methods:

  • set(nextValue)
  • update((previous) => nextValueFromPrevious(previous))

You can check the implementation here

Here’s good ol’ Counter example.

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>
  );
}

Example of a derived state

Let's take the example of a todo details page.

type Props = { todoId: number }
export function TodoPage({ todoId }: Props) {
  const todoState = fetchTodo(todoId)
  switch (todoState.status) {
    case 'pending': return <div>Loading…</div>
    case 'failure': return <div>Uh-oh, something went wrong</div>
    case 'success': return <Todo todo={todoState.todo} />
  }
}

function Todo({ todo }) {
  return (
    <ul>
      <li>id: {todo.id}</li>
      <li>title: {todo.title}</li></ul>
  )
}

Example of connected/disconnected lifecycles

This is the equivalent of React’s useEffect or Vue’s onMount/onUnmount.

Use onConnected(element, callback) and onDisconnected(element, callback)

Note

At this stage the functions signatures can still be discussed

import { State, onConnected, onDisconnected } from '@sacdenoeuds/yawn'

export function SomeOverlay() {
  const init = (element: HTMLElement) => {
    let unsubscribe = () => {}
    onConnected(element, () => {
      unsubscribe = onClickOutside(element, () => )
    })
    onDisconnected(element, () => unsubscribe())
  }

  return (
    <div ref={init}>
      {formatTime(time)}
    </div>
  )
}

Because it is pure & standard JavaScript, the pros are massive:

  1. No need to learn yet-another-library, just plain ol' standard JavaScript. Your knowledge will remain forever (bye bye JavaScript fatigue).
  2. You get all the ecosystem for free ; (soon-to-come) helpers like map, filter, reduce, take, etc of the iterator protocol. Polyfill already available via core-js.
  3. Any library helper for async iterators will work out-of-the-box. A bunch of them are pointed out in the proposal. Another worth mentioning is fx-ts
  4. Such a reactivity system weighs literally just a few bytes compared to 10+kB gzipped of existing solutions like Vue's, see the implementation.
  5. Async iterables are lazily evaluated, therefore super efficient and result in a fine-grained reactivity, pull-based like signals: if you don't for await (…) nothing happens.
  6. Some objects are already implementing the async iterator protocol like WebSocket, Server-Sent Events, ReadableStream, etc., you could even drive your frontend state from your backend streams !
  7. The door is wide opened for state history or state travel using Iterable helpers.
  8. One cannot forget to track a dependency: there is no access to the current value without iterating on the iterable.

This approach looks promising but still requires exploration regarding rendering an array state for instance, or composing JSX from a Server-Sent Event state. Checking memory leaks is clearly on the radar too.

Async Iterators is a difficult technology to master, it has pitfalls and traps. Compared to Signals, automatic dependency tracking is lost, I consider this to actually be a pro but it still is a missing feature compared to Signals.

Finally, yet another JSX runtime must be created to support Async Iterables as attribute values or children.

Which I did and it weighs 1.5kB gzipped. The JSX runtime implementation's design leaves room for future changes like hydration.

Again, if you are interested in pushing this further, please star the project. If the repo reaches 5,000+ stars I will start making it production-ready.

You can also help out or get in touch on GitHub by creating an issue ☺️.

If you are interested in contributing, take a look at values, it contains expectations & philosophies I have regarding frontend libraries or frameworks.

Thanks 💛

联系我们 contact @ memedata.com