Add async computed
This PR introduces two new utility functions for handling asynchronous computed values in Preact applications: asyncComputed and useAsyncComputed. These helpers make it easier to work with async data in a reactive way while maintaining proper TypeScript support and Suspense compatibility.
asyncComputed<T>
A utility function that creates a signal that computes its value asynchronously. It handles both synchronous and asynchronous computations while providing proper error handling and state management.
const userDataSignal = asyncComputed(async () => {
const response = await fetch('/api/user');
return response.json();
});
// Access the computed value
console.log(userDataSignal.value);
// Access any error that occurred
console.log(userDataSignal.error.value);
Key features:
- Type-safe error handling
- Automatic cleanup of stale computations
- Support for both sync and async computed values
- Proper handling of computation race conditions
Implementation Details
- Uses a Map-based caching system for suspenseful value storage
- Provides proper TypeScript generics support
Question: When the input variables change for computed would you expect us to erase data and error or should we retain it and fetch in the background? Alternatively we can add a fetching signal to have a background indicator.
EDIT: Added running as a way to differentiate between things
React
In theory this should support React Suspense as well as we keep a shallow cache for the computeds, the issue being that this is based on useId() which isn't readily available in React 18 according to the types. When the minimum peer-dependency becomes React v19 we could add this for React as well, or we come up with a different way to track our key in the computed-cache.
π¦ Changeset detected
Latest commit: 4c7eaccfc3092d750dd807a77a05f238d5ce5a2a
The changes in this PR will be included in the next version bump.
This PR includes changesets to release 1 package
| Name | Type |
|---|---|
| @preact/signals | Minor |
Not sure what this means? Click here to learn what changesets are.
Click here if you're a maintainer who wants to add another changeset to this PR
Deploy Preview for preact-signals-demo ready!
| Name | Link |
|---|---|
| Latest commit | 4c7eaccfc3092d750dd807a77a05f238d5ce5a2a |
| Latest deploy log | https://app.netlify.com/projects/preact-signals-demo/deploys/683703ca38810c0008922ce2 |
| Deploy Preview | https://deploy-preview-686--preact-signals-demo.netlify.app |
| Preview on mobile | Toggle QR Code...Use your smartphone camera to open QR code link. |
To edit notification comments on pull requests, go to your Netlify project configuration.
Size Change: 0 B
Total Size: 84.5 kB
βΉοΈ View Unchanged
| Filename | Size |
|---|---|
docs/dist/assets/bench.********.js |
1.59 kB |
docs/dist/assets/client.********.js |
46.2 kB |
docs/dist/assets/index.********.js |
1.09 kB |
docs/dist/assets/jsxRuntime.module.********.js |
283 B |
docs/dist/assets/preact.module.********.js |
4.01 kB |
docs/dist/assets/signals-core.module.********.js |
1.5 kB |
docs/dist/assets/signals.module.********.js |
2.04 kB |
docs/dist/assets/style.********.js |
21 B |
docs/dist/assets/style.********.css |
1.24 kB |
docs/dist/basic-********.js |
243 B |
docs/dist/demos-********.js |
4.32 kB |
docs/dist/nesting-********.js |
1.13 kB |
docs/dist/react-********.js |
239 B |
packages/core/dist/signals-core.js |
1.53 kB |
packages/core/dist/signals-core.mjs |
1.55 kB |
packages/preact/dist/signals.js |
1.57 kB |
packages/preact/dist/signals.mjs |
1.53 kB |
packages/react-transform/dist/signals-*********.js |
4.9 kB |
packages/react-transform/dist/signals-transform.mjs |
4.13 kB |
packages/react-transform/dist/signals-transform.umd.js |
5.01 kB |
packages/react/dist/signals.js |
188 B |
packages/react/dist/signals.mjs |
150 B |
Would it be helpful for future library users to point out pre-emptively in the documentation that async computeds will be reactive until the first await? For example:
asyncComputed(async () => {
const client = await authClient();
const user = await client.getUser(userId.value);
return user;
});
In this made-up example the async computed won't update when the userId signal changes. This case could be fixed by e.g. front-loading the dependencies:
asyncComputed(async () => {
const id = userId.value;
const client = await authClient();
const user = await client.getUser(id);
return user;
});
Yes, makes sense to explicitly document this. I thought about it but apparently did not write it π
EDIT: ah it's because I kind of did in the blog post I wrote today
Hi!
Have you considered using AbortController instead of computeCount?
You can also expose it to the end user like this:
const userDataSignal = asyncComputed(async ({ abortSignal }) => {
const response = await fetch('/api/user', { signal: abortSignal });
return response.json();
});
This approach is used, for example, in react-query.
The difference between this primitive and react-query is that it can be used for more than just fetch, consider webworker traffic and other use cases.
True, itβs broader than fetch, but AbortSignal is also supported in streams, locks, and more. Itβs especially useful for 2β3 chained fetch calls or other long tasks where you need to handle cancellation properly.