Skip to content

Add async computed with async callbacks - #686

Open
JoviDeCroock wants to merge 6 commits into
mainfrom
async-computed
Open

Add async computed with async callbacks#686
JoviDeCroock wants to merge 6 commits into
mainfrom
async-computed

Conversation

@JoviDeCroock

@JoviDeCroock JoviDeCroock commented May 27, 2025

Copy link
Copy Markdown
Member

Summary

  • add an experimental core asyncComputed() primitive for ordinary synchronous or async callbacks
  • expose reactive pending, settled, failed, and error state while retaining the last successful value and ignoring stale results
  • make async computeds created by createModel() automatically follow model disposal
  • add useAsyncComputed() to the Preact and React adapters, including error-boundary behavior and Suspense for externally/model-owned instances
  • defer hook-owned callback work until commit so abandoned and server renders do not start it

Design choice

This is the ordinary async/await option. Signal reads are tracked synchronously, so reactive inputs must be captured before the first await:

const user = asyncComputed(async () => {
  const id = userId.value;
  const response = await fetch(`/api/users/${id}`);
  return response.json();
});

The alternative generator-based prototype re-enters dependency tracking after each yield. Keeping these separate lets us compare familiar async ergonomics against complete cross-suspension tracking.

Hook-created instances render their explicit state and cannot suspend on initial mount because framework hook state does not survive that suspension. For Suspense, create the async computed in a model or above the boundary and pass the instance to useAsyncComputed(instance, { suspend: true }).

@changeset-bot

changeset-bot Bot commented May 27, 2025

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2483777

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@preact/signals-core Minor
@preact/signals Minor
@preact/signals-react Minor
preact-signals-devtools Patch

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

@netlify

netlify Bot commented May 27, 2025

Copy link
Copy Markdown

Deploy Preview for preact-signals-demo ready!

Name Link
🔨 Latest commit 4c7eacc
🔍 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.

@github-actions

github-actions Bot commented May 27, 2025

Copy link
Copy Markdown
Contributor

Size Change: -6.26 kB (-3.02%)

Total Size: 201 kB

📦 View Changed
Filename Size Change
docs/dist/assets/bench-********.js 1.6 kB +1 B (+0.06%)
docs/dist/assets/client-********.js 46.6 kB -1 B (0%)
docs/dist/assets/devtools-********.js 914 B +3 B (+0.33%)
docs/dist/assets/EmbeddedDevtools-********.js 23.1 kB +1 B (0%)
docs/dist/assets/index-********.js 1.2 kB -7.31 kB (-85.86%) 🏆
docs/dist/assets/signals-core.module-********.js 1.92 kB +1 B (+0.05%)
docs/dist/assets/signals.module-********.js 2.66 kB -3 B (-0.11%)
docs/dist/assets/Unmount-********.js 650 B -1 B (-0.15%)
docs/dist/assets/utils.module-********.js 521 B +1 B (+0.19%)
docs/dist/basic-********.js 246 B -1 B (-0.4%)
docs/dist/nesting-********.js 1.15 kB -1 B (-0.09%)
docs/dist/react-********.js 243 B +1 B (+0.41%)
packages/core/dist/signals-core.js 2.2 kB +259 B (+13.37%) ⚠️
packages/core/dist/signals-core.mjs 2.19 kB +260 B (+13.46%) ⚠️
packages/preact/dist/signals.js 2.08 kB +257 B (+14.11%) ⚠️
packages/preact/dist/signals.mjs 2.01 kB +250 B (+14.2%) ⚠️
packages/react/dist/signals.js 227 B +13 B (+6.07%) 🔍
packages/react/dist/signals.mjs 175 B +10 B (+6.06%) 🔍
ℹ️ View Unchanged
Filename Size
docs/dist/assets/jsxRuntime.module-********.js 300 B
docs/dist/assets/preact.module-********.js 4.74 kB
docs/dist/assets/style-********.css 7.19 kB
packages/debug/dist/debug.js 4.72 kB
packages/debug/dist/debug.mjs 4.24 kB
packages/devtools-adapter/dist/devtools-adapter.js 2.36 kB
packages/devtools-adapter/dist/devtools-adapter.mjs 2.07 kB
packages/devtools-ui/dist/devtools-ui.js 21.3 kB
packages/devtools-ui/dist/devtools-ui.mjs 20.6 kB
packages/preact-transform/dist/signals-transform.js 1.66 kB
packages/preact-transform/dist/signals-transform.mjs 1.61 kB
packages/preact-transform/dist/signals-transform.umd.js 1.77 kB
packages/react-transform/dist/signals-transform.js 7.63 kB
packages/react-transform/dist/signals-transform.mjs 6.78 kB
packages/react-transform/dist/signals-transform.umd.js 7.75 kB
packages/vite-plugin/dist/vite-plugin.js 8.86 kB
packages/vite-plugin/dist/vite-plugin.mjs 7.86 kB

compressed-size-action

@jviide

jviide commented May 30, 2025

Copy link
Copy Markdown
Contributor

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

@JoviDeCroock

JoviDeCroock commented May 30, 2025

Copy link
Copy Markdown
Member Author

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

@mxck

mxck commented May 31, 2025

Copy link
Copy Markdown

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.

@JoviDeCroock

Copy link
Copy Markdown
Member Author

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.

@mxck

mxck commented May 31, 2025

Copy link
Copy Markdown

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.

@rschristian rschristian left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hype hype hype, probably one of the most requested features? Would be awesome to see!

the issue being that this is based on useId() which isn't readily available in React 18 according to the types

useId came out w/ React 18.0, types are probably incorrect there.

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

That sounds like a great solution to me.

# Conflicts:
#	packages/preact/utils/src/index.ts
#	packages/preact/utils/test/browser/index.test.tsx
@JoviDeCroock JoviDeCroock changed the title Add async computed Add async computed with async callbacks Sep 3, 2026
@JoviDeCroock JoviDeCroock reopened this Sep 3, 2026
@netlify

netlify Bot commented Sep 3, 2026

Copy link
Copy Markdown

Deploy Preview for preact-signals-demo ready!

Name Link
🔨 Latest commit 2483777
🔍 Latest deploy log https://app.netlify.com/projects/preact-signals-demo/deploys/6a992a8d9df73600081e331f
😎 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.
🤖 Make changes Run an agent on this branch

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants