|
| 1 | +# Frameworks & AsyncContext |
| 2 | + |
| 3 | +Many popular web frameworks are eagerly waiting on the `AsyncContext` proposal, either to improve experience for their users (application developers) or to reduce footguns. |
| 4 | + |
| 5 | +This document lists the concrete reasons that different frameworks have for using `AsyncContext`. |
| 6 | + |
| 7 | +Note: **[client]** and **[server]** markers in messages have been added by the proposal's champions. |
| 8 | + |
| 9 | +--- |
| 10 | + |
| 11 | +## [Angular](https://angular.dev/) |
| 12 | + |
| 13 | +_TODO_ |
| 14 | + |
| 15 | +## [Pruvious](https://pruvious.com/) |
| 16 | + |
| 17 | +> Pruvious is a CMS built on top of Nuxt. It can operate in a classic Node.js environment as well as in Cloudflare Workers, which run on the V8 engine with a limited subset of Node.js. |
| 18 | +> **[server]** I believe that async context is crucial for providing an excellent developer experience in a CMS. Developers need consistent access to the currently logged-in user, the context language, and the request itself. Without this, it would be overwhelmingly complicated for developers to pass the current request event to each function provided by the CMS. This is why async context is heavily utilized in Pruvious. |
| 19 | +> **[client]** While this works well on the server side, async context is unfortunately not universal. For instance, Pruvious users (developers) cannot reproduce issues in StackBlitz. If async context were supported in browsers, Pruvious could run in the browser just like Nuxt does. In addition to issue reproduction, I believe that running the CMS in the browser would greatly simplify the learning-by-example process for new users. |
| 20 | +> <small>Muris Ceman, Pruvious maintainer</small> |
| 21 | +
|
| 22 | +## [React](https://react.dev/) |
| 23 | + |
| 24 | +The following is a quote [from the React docs](https://react.dev/reference/react/useTransition#troubleshooting) showing a developer error that is common enough to be included in their documentation, and that would be solved by browsers providing `AsyncContext` support. |
| 25 | + |
| 26 | +> ### Troubleshooting |
| 27 | +> #### React doesn’t treat my state update after `await` as a Transition |
| 28 | +> |
| 29 | +> **[client]** When you use await inside a startTransition function, the state updates that happen after the await are not marked as Transitions. You must wrap state updates after each await in a startTransition call: |
| 30 | +> |
| 31 | +> ```javascript |
| 32 | +> startTransition(async () => { |
| 33 | +> await someAsyncFunction(); |
| 34 | +> // ❌ Not using startTransition after await |
| 35 | +> setPage('/about'); |
| 36 | +> }); |
| 37 | +> ``` |
| 38 | +> |
| 39 | +> However, this works instead: |
| 40 | +> |
| 41 | +> ```javascript |
| 42 | +> startTransition(async () => { |
| 43 | +> await someAsyncFunction(); |
| 44 | +> // ✅ Using startTransition *after* await |
| 45 | +> startTransition(() => { |
| 46 | +> setPage('/about'); |
| 47 | +> }); |
| 48 | +> }); |
| 49 | +> ``` |
| 50 | +> |
| 51 | +> This is a JavaScript limitation due to React losing the scope of the async context. In the future, when AsyncContext is available, this limitation will be removed. |
| 52 | +
|
| 53 | +_Todo:_ Also include a quote from a person |
| 54 | +
|
| 55 | +## [Solid](https://www.solidjs.com/) |
| 56 | +
|
| 57 | +> We're pretty excited about having standard AsyncContext API. |
| 58 | +> |
| 59 | +> ### Server |
| 60 | +> |
| 61 | +> Currently in Solid we use AsyncLocalStorage on the server in SolidStart our metaframework as a means of RequestEvent and ResponseEvent injection. We find this so important it is built into the core of Solid. And leveraged in a few important ways. |
| 62 | +> |
| 63 | +> First, Our Server Functions (compiled RPCs denoted by `"use server"`) have an isomorphic type safe interface intended to swap into any existing client side API. This means getting the Request in isn't part of the function signature and needs to be injected. |
| 64 | +> |
| 65 | +> Secondly, we need a mechanism for ecosystem libraries to tap into the request so provide important metadata. For example the Solid Router exporting matches and having a place to put the Response(say to handle redirects on the server) or registering of assets. Its important enough for us to handle this core so we don't fracture the metaframework ecosystem and all libraries can work regardless of which you choose (SolidStart, Tanstack or whatever) |
| 66 | +> |
| 67 | +> We've manufactured mechanisms for this in the past but they required special wrappers because you can't resume our context on the other side of an `await` when inside user code. |
| 68 | +> |
| 69 | +> While we have been fortunate that most platforms have polyfilled AsyncLocalStorage it remains difficult for platforms like Stackblitz which is built on webcontainers and is relying on AsyncContext to bring these features to the browser environments. To this day while we run most examples on Stackblitz their capability is greatly reduced impacting it's ability to act as good place to reproduce and build SolidStart projects. |
| 70 | +> |
| 71 | +> ### Client |
| 72 | +> |
| 73 | +> Modern JS frameworks work off context/synchronous scope. This is even more pronounced in Signals based frameworks because there is often both the tracking scope (ie collecting dependencies) and the ownership context which collects nested Signals to handle automatic disposal. |
| 74 | +> |
| 75 | +> Once you go async you lose both contexts. For the most part this is OK. From Solid's perspective tracking is synchronous by design. Some Signals libraries will want to continue tracking after: |
| 76 | +> ```javascript |
| 77 | +> createEffect(async () => { |
| 78 | +> const value1 = inputSignal() // track dep |
| 79 | +> const asyncValue = await fetch(value1); |
| 80 | +> const value2 = inputSignal2(); // can we track here?? |
| 81 | +> const asyncValue2 = await fetch(asyncValue, value2); |
| 82 | +> doEffect(asyncValue2) |
| 83 | +> }) |
| 84 | +> ``` |
| 85 | +> |
| 86 | +> But ownership would definitely benefit from being able to re-inject our context back in. The potential applications honestly are numerous. `await` in user code without special wrappers, resuming async sequences like in our Transaction or Transition API, pausing and resuming hydration during streaming. |
| 87 | +> |
| 88 | +> There is a world where we'd just use this mechanism as our core context mechanism. I have performance concerns there which I wouldn't take lightly, but mechanically we are just remaking this in every JavaScript framework and given where things are going I only expect to see more of this. |
| 89 | +> <small>Ryan Carniato, Solid maintainer</small> |
| 90 | +
|
| 91 | +## [Svelte](https://svelte.dev/) |
| 92 | +
|
| 93 | +<!-- |
| 94 | +> **[client]** We would like to add support for async state to Svelte 5. For example, we would like to allow `await` at the top level of Svelte component. |
| 95 | +> However because we use signal reactivity for tracking, and `await` means losing the reactive context, we would have to leverage the fact that Svelte is a compiler and essentially wrap the `await` expressions so we resume it magically. If we had AsyncContext, we would be use to rely on the native JavaScript semantics, rather than having to work them around through compilation. |
| 96 | +> <small>Dominic Gannaway, Svelte maintainer</small> |
| 97 | +--> |
| 98 | +
|
| 99 | +> The Svelte team are eagerly awaiting the day we can use `AsyncContext`. The widespread adoption of `AsyncLocalStorage` across different packages (and runtimes, despite its non-standard status) is clear evidence that real use cases exist; there is no a priori reason to assume that those use cases are restricted to server runtimes, and indeed there are two concrete examples where our hands are currently tied by the lack of this capability in the browser: **[server]** we're introducing a `getRequestEvent` function in SvelteKit that allows functions on the server to read information about the current request context (including things like the requested URL, cookies, headers etc), even if the function isn't called synchronously (which is necessary for it to be generally useful). |
| 100 | +> |
| 101 | +> **[client]** Ideally we would have a similar function, `getNavigationEvent`, which would apply similarly to client-side navigations; this is currently impossible as reactivity in Svelte is signal-based. The dependencies of a given reaction are determined by noting which signals are read when the reaction executes. We are working on a new asynchronous reactivity model, which requires that dependencies can be tracked even if they are read after the initial execution (for example `<p>{await a + await b}</p>` should depend on both `a` and `b`). As a compiler-based framework, we can fudge this by transforming the `await` expressions, but we can only do this in certain contexts, leading to confusing discrepancies. Other frameworks don't even have this option, and must resort to an inferior developer experience instead. |
| 102 | +> Given these, and other use cases that we anticipate will emerge, we fully support the AsyncContext proposal. |
| 103 | +> <small>Rich Harris, Svelte maintainer</small> |
| 104 | +
|
| 105 | +There are [good examples on Reddit](https://www.reddit.com/r/sveltejs/comments/1gyqf27/svelte_5_runes_async_a_match_made_in_hell/) of Svelte users frustrated because it's not able to preserve context through async operations. |
| 106 | +
|
| 107 | +The missing async support is also explicitly called out [in their documentation](https://svelte.dev/docs/svelte/$effect#Understanding-dependencies): |
| 108 | +
|
| 109 | +> `$effect` automatically picks up any reactive values (`$state`, `$derived`, `$props`) that are synchronously read inside its function body (including indirectly, via function calls) and registers them as dependencies. When those dependencies change, the `$effect` schedules a re-run. |
| 110 | +> |
| 111 | +> Values that are read _asynchronously_ — after an `await` or inside a `setTimeout`, for example — will not be tracked. Here, the canvas will be repainted when color changes, but not when size changes: |
| 112 | +> |
| 113 | +> ```javascript |
| 114 | +> $effect(() => { |
| 115 | +> const context = canvas.getContext('2d'); |
| 116 | +> context.clearRect(0, 0, canvas.width, canvas.height); |
| 117 | +> |
| 118 | +> // this will re-run whenever `color` changes... |
| 119 | +> context.fillStyle = color; |
| 120 | +> |
| 121 | +> setTimeout(() => { |
| 122 | +> // ...but not when `size` changes |
| 123 | +> context.fillRect(0, 0, size, size); |
| 124 | +> }, 0); |
| 125 | +> }); |
| 126 | +> ``` |
| 127 | +
|
| 128 | +## [Vue](https://vuejs.org/) |
| 129 | +
|
| 130 | +> AsyncContext is an important feature to have in JavaScript ecosystem. Patterns like singleton is a very common practice in many languages and frameworks. Things that `getCurrentComponent()` relying on a global singleton state work fine by introducing a stack in sync operations, but is becoming very challenging in async flows, there concurrent access to the global state will lead to race conditions. It currently has no workaround in JavaScript without a compiler magic (with a lot of false negatives). |
| 131 | +> |
| 132 | +> **[client]** Frameworks like Vue provides lifecycle hooks that requires such information, consider Vue 3 support mounting multiple apps at the same time, and some components can be async, the async context race conditions become a headache to us. So that we have to introduce the compiler magic to make it less mental burden to the users. **[server]** Similar stories happen in Nuxt and Nitro on the server side, where the server need to handle concurrent inbound requests, without a proper AsyncContext support, we are also having the risk to leaking information across different requests. |
| 133 | +> |
| 134 | +> **[client]** As "hooks" is also becoming a very popular API design for many frameworks, including React, Solid, Vue and so on. All these usage would more or less limited by the lack of AsyncContext. Specially since there is no easy runtime workaround/polyfill, I believe it's an essential feature that JavaScript is currently lacking |
| 135 | +> <small>Anthony Fu, Vue maintainer</small> |
| 136 | +
|
| 137 | +Vue currently has a transpiler that, at least for async/await, allows [emulating AsyncContext-like behavior](https://github.com/vuejs/core/blob/d48937fb9550ad04b1b28c805ecf68c665112412/packages/runtime-core/src/apiSetupHelpers.ts#L480-L498): |
| 138 | +
|
| 139 | +> We've actually been keeping an eye on that proposal for a while now. We have two very suitable use cases for it: |
| 140 | +> 1. **[client]** _Restoring component context in an async setup flow._ |
| 141 | +> In Vue, components can have an async `setup()` function that returns a `Promise`. But this creates a trap when using composables (equivalent of React hooks) that require an active component context: |
| 142 | +> ```javascript |
| 143 | +> useFoo() |
| 144 | +> await 1 |
| 145 | +> useBar() // context lost |
| 146 | +> ``` |
| 147 | +> Right now we can work around this by doing compiler transforms like this: |
| 148 | +> ```javascript |
| 149 | +> let __temp, __restore |
| 150 | +> |
| 151 | +> useFoo() |
| 152 | +> // transformed |
| 153 | +> ;( |
| 154 | +> ([__temp,__restore] = _withAsyncContext(() => 1)), |
| 155 | +> await __temp, |
| 156 | +> __restore() |
| 157 | +> ) |
| 158 | +> useBar() |
| 159 | +> ``` |
| 160 | +> But this only works when there is a build step with Vue single-file components, and does not work in plain JS. `AsyncContext` would allow us to use a native mechanism that works consistently in all cases. |
| 161 | +> 2. **[client / devtools]** _Tracking state mutation during async actions in state management._ |
| 162 | +> Our official state management lib Pinia (https://pinia.vuejs.org/) has a devtools integration that is able to trace action invocations, but currently there is no way to associate async state mutations to the owner action. A single action may trigger multiple state mutations at different times: |
| 163 | +> ```javascript |
| 164 | +> actions: { |
| 165 | +> async doSomething() { |
| 166 | +> // mutation 1 |
| 167 | +> this.data = await api.post(...) |
| 168 | +> // mutation 2 |
| 169 | +> this.user = await api.get(this.data.id) |
| 170 | +> } |
| 171 | +> } |
| 172 | +> ``` |
| 173 | +> We want to be able to link each mutation triggered by `doSomething` to it and visualize it in the devtools. Again currently the only way to do it is compiler-based code instrumentations, but we don't want to add extra overhead to plain JS files. `AsyncContext` would make this easier without relying on compilers. |
| 174 | +> |
| 175 | +> <small>Evan You, Vue maintainer</small> |
| 176 | +
|
| 177 | +## Wiz |
| 178 | +
|
| 179 | +> [!WARNING] |
| 180 | +> Wiz is a Google-internal framework, not open source. |
| 181 | +
|
| 182 | +> Wiz is a Google-internal web application framework designed to meet the requirements of Google-scale applications. It focuses on performance, supporting lazy code loading for fast user response times and server-side rendering for fast initial page loads. Wiz offers high performance across the widest range of browsers, devices, and connection speeds. |
| 183 | +> |
| 184 | +> The Wiz team anticipates AsyncContext to be a critical component to instrumenting tracing in our signals-based framework. Tracing has been a top request to help users gain insight into the performance characteristics of specific user interactions. This work is currently underway and it's already evident that AsyncContext allows propagating important contextual information to async APIs that run user-provided callbacks. This is a very common design pattern in the framework and without the ability to propagate data across async boundaries, tracing would not be possible. |
0 commit comments