signals icon indicating copy to clipboard operation
signals copied to clipboard

Add async computed

Open JoviDeCroock opened this issue 10 months ago β€’ 8 comments

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.

JoviDeCroock avatar May 27 '25 10:05 JoviDeCroock

πŸ¦‹ 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

changeset-bot[bot] avatar May 27 '25 10:05 changeset-bot[bot]

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...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

netlify[bot] avatar May 27 '25 10:05 netlify[bot]

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

compressed-size-action

github-actions[bot] avatar May 27 '25 10:05 github-actions[bot]

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

jviide avatar May 30 '25 19:05 jviide

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

JoviDeCroock avatar May 30 '25 19:05 JoviDeCroock

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.

mxck avatar May 31 '25 10:05 mxck

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.

JoviDeCroock avatar May 31 '25 10:05 JoviDeCroock

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.

mxck avatar May 31 '25 11:05 mxck