diff --git a/.changeset/silly-jobs-hide.md b/.changeset/silly-jobs-hide.md new file mode 100644 index 00000000000..4be123083fd --- /dev/null +++ b/.changeset/silly-jobs-hide.md @@ -0,0 +1,5 @@ +--- +'@builder.io/qwik': minor +--- + +feat: add `useAsyncComputed$`. Use it instead of `useComputed$` when the computation is async. There is a `track()` function to ensure tracking of signals. diff --git a/packages/docs/src/routes/api/qwik/api.json b/packages/docs/src/routes/api/qwik/api.json index 367fade3081..87205fe8dd5 100644 --- a/packages/docs/src/routes/api/qwik/api.json +++ b/packages/docs/src/routes/api/qwik/api.json @@ -562,6 +562,20 @@ "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/render/jsx/types/jsx-qwik-attributes.ts", "mdFile": "qwik.correctedtoggleevent.md" }, + { + "name": "createAsyncComputed$", + "id": "createasynccomputed_", + "hierarchy": [ + { + "name": "createAsyncComputed$", + "id": "createasynccomputed_" + } + ], + "kind": "Function", + "content": "Returns read-only signal that updates when signals used in the `AsyncComputedFn` change. Unlike useAsyncComputed$, this is not a hook and it always creates a new signal.\n\n\n```typescript\ncreateAsyncComputed$: (qrl: AsyncComputedFn) => Signal>\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nqrl\n\n\n\n\nAsyncComputedFn<T>\n\n\n\n\n\n
\n\n**Returns:**\n\n[Signal](#signal)<Awaited<T>>", + "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-task.ts", + "mdFile": "qwik.createasynccomputed_.md" + }, { "name": "createComputed$", "id": "createcomputed_", @@ -1774,7 +1788,7 @@ } ], "kind": "Function", - "content": "> This API is provided as an alpha preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.\n> \n\n> Warning: This API is now obsolete.\n> \n> This is no longer needed as the preloading happens automatically in qrl-class.ts. Leave this in your app for a while so it uninstalls existing service workers, but don't use it for new projects.\n> \n\n\n```typescript\nPrefetchServiceWorker: (opts: {\n base?: string;\n scope?: string;\n path?: string;\n verbose?: boolean;\n fetchBundleGraph?: boolean;\n nonce?: string;\n}) => JSXNode<'script'>\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nopts\n\n\n\n\n{ base?: string; scope?: string; path?: string; verbose?: boolean; fetchBundleGraph?: boolean; nonce?: string; }\n\n\n\n\n\n
\n\n**Returns:**\n\n[JSXNode](#jsxnode)<'script'>", + "content": "> This API is provided as an alpha preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.\n> \n\n> Warning: This API is now obsolete.\n> \n> This is no longer needed as the preloading happens automatically in qrl-class.ts. Leave this in your app for a while so it uninstalls existing service workers, but don't use it for new projects.\n> \n\n\n```typescript\nPrefetchServiceWorker: (opts: {\n base?: string;\n scope?: string;\n path?: string;\n verbose?: boolean;\n fetchBundleGraph?: boolean;\n nonce?: string;\n}) => JSXNode<'script'>\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nopts\n\n\n\n\n{ base?: string; scope?: string; path?: string; verbose?: boolean; fetchBundleGraph?: boolean; nonce?: string; }\n\n\n\n\n\n
\n\n**Returns:**\n\nJSXNode<'script'>", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/components/prefetch.ts", "mdFile": "qwik.prefetchserviceworker.md" }, @@ -3024,6 +3038,20 @@ "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/state/common.ts", "mdFile": "qwik.unwrapstore.md" }, + { + "name": "useAsyncComputed$", + "id": "useasynccomputed_", + "hierarchy": [ + { + "name": "useAsyncComputed$", + "id": "useasynccomputed_" + } + ], + "kind": "Function", + "content": "Returns a computed signal which is calculated from the given async function. A computed signal is a signal which is calculated from other signals. When the signals change, the computed signal is recalculated, and if the result changed, all tasks which are tracking the signal will be re-run and all components that read the signal will be re-rendered.\n\nThe function can be asynchronous and receives a `track` function to observe changes.\n\n\n```typescript\nuseAsyncComputed$: (qrl: AsyncComputedFn) => Signal>\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nqrl\n\n\n\n\nAsyncComputedFn<T>\n\n\n\n\n\n
\n\n**Returns:**\n\n[Signal](#signal)<Awaited<T>>", + "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-task.ts", + "mdFile": "qwik.useasynccomputed_.md" + }, { "name": "useComputed$", "id": "usecomputed_", @@ -3034,7 +3062,7 @@ } ], "kind": "Function", - "content": "Returns a computed signal which is calculated from the given function. A computed signal is a signal which is calculated from other signals. When the signals change, the computed signal is recalculated, and if the result changed, all tasks which are tracking the signal will be re-run and all components that read the signal will be re-rendered.\n\nThe function must be synchronous and must not have any side effects.\n\nAsync functions are deprecated because:\n\n- When calculating the first time, it will see it's a promise and it will restart the render function. - Qwik can't track used signals after the first await, which leads to subtle bugs. - Both `useTask$` and `useResource$` are available, without these problems.\n\nIn v2, async functions won't work.\n\n\n```typescript\nuseComputed$: (qrl: ComputedFn) => Signal>\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nqrl\n\n\n\n\n[ComputedFn](#computedfn)<T>\n\n\n\n\n\n
\n\n**Returns:**\n\n[Signal](#signal)<Awaited<T>>", + "content": "Returns a computed signal which is calculated from the given function. A computed signal is a signal which is calculated from other signals. When the signals change, the computed signal is recalculated, and if the result changed, all tasks which are tracking the signal will be re-run and all components that read the signal will be re-rendered.\n\nThe function must be synchronous and must not have any side effects. If you need a version that accepts async functions, use `useAsyncComputed$`.\n\n\n```typescript\nuseComputed$: (qrl: ComputedFn) => Signal>\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nqrl\n\n\n\n\n[ComputedFn](#computedfn)<T>\n\n\n\n\n\n
\n\n**Returns:**\n\n[Signal](#signal)<Awaited<T>>", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-task.ts", "mdFile": "qwik.usecomputed_.md" }, diff --git a/packages/docs/src/routes/api/qwik/index.mdx b/packages/docs/src/routes/api/qwik/index.mdx index 6553efd467b..c03075369c1 100644 --- a/packages/docs/src/routes/api/qwik/index.mdx +++ b/packages/docs/src/routes/api/qwik/index.mdx @@ -1701,6 +1701,46 @@ Description [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/render/jsx/types/jsx-qwik-attributes.ts) +## createAsyncComputed$ + +Returns read-only signal that updates when signals used in the `AsyncComputedFn` change. Unlike useAsyncComputed$, this is not a hook and it always creates a new signal. + +```typescript +createAsyncComputed$: (qrl: AsyncComputedFn) => Signal>; +``` + + + +
+ +Parameter + + + +Type + + + +Description + +
+ +qrl + + + +AsyncComputedFn<T> + + + +
+ +**Returns:** + +[Signal](#signal)<Awaited<T>> + +[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-task.ts) + ## createComputed$ > Warning: This API is now obsolete. @@ -3667,7 +3707,7 @@ opts **Returns:** -[JSXNode](#jsxnode)<'script'> +JSXNode<'script'> [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/components/prefetch.ts) @@ -10156,17 +10196,53 @@ T [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/state/common.ts) -## useComputed$ +## useAsyncComputed$ -Returns a computed signal which is calculated from the given function. A computed signal is a signal which is calculated from other signals. When the signals change, the computed signal is recalculated, and if the result changed, all tasks which are tracking the signal will be re-run and all components that read the signal will be re-rendered. +Returns a computed signal which is calculated from the given async function. A computed signal is a signal which is calculated from other signals. When the signals change, the computed signal is recalculated, and if the result changed, all tasks which are tracking the signal will be re-run and all components that read the signal will be re-rendered. + +The function can be asynchronous and receives a `track` function to observe changes. + +```typescript +useAsyncComputed$: (qrl: AsyncComputedFn) => Signal>; +``` -The function must be synchronous and must not have any side effects. + + +
-Async functions are deprecated because: +Parameter -- When calculating the first time, it will see it's a promise and it will restart the render function. - Qwik can't track used signals after the first await, which leads to subtle bugs. - Both `useTask$` and `useResource$` are available, without these problems. + + +Type + + + +Description + +
+ +qrl + + + +AsyncComputedFn<T> + + + +
+ +**Returns:** + +[Signal](#signal)<Awaited<T>> + +[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-task.ts) + +## useComputed$ + +Returns a computed signal which is calculated from the given function. A computed signal is a signal which is calculated from other signals. When the signals change, the computed signal is recalculated, and if the result changed, all tasks which are tracking the signal will be re-run and all components that read the signal will be re-rendered. -In v2, async functions won't work. +The function must be synchronous and must not have any side effects. If you need a version that accepts async functions, use `useAsyncComputed$`. ```typescript useComputed$: (qrl: ComputedFn) => Signal>; diff --git a/packages/docs/src/routes/docs/(qwik)/components/state/index.mdx b/packages/docs/src/routes/docs/(qwik)/components/state/index.mdx index 1671c0448ca..ece05d38707 100644 --- a/packages/docs/src/routes/docs/(qwik)/components/state/index.mdx +++ b/packages/docs/src/routes/docs/(qwik)/components/state/index.mdx @@ -224,6 +224,7 @@ export default component$(() => { In Qwik, there are two ways to create computed values, each with a different use case (in order of preference): 1. `useComputed$()`: `useComputed$()` is the preferred way of creating computed values. Use it when the computed value can be derived synchronously purely from the source state (current application state). For example, creating a lowercase version of a string or combining first and last names into a full name. + There is also an async version of `useComputed$()`, called `useAsyncComputed$()`. It allows passing async functions and the signal value is the resolved result. 2. [`useResource$()`](/docs/(qwik)/components/state/index.mdx#useresource): `useResource$()` is used when the computed value is asynchronous or the state comes from outside of the application. For example, fetching the current weather (external state) based on a current location (application internal state). @@ -260,6 +261,16 @@ export default component$(() => { > **NOTE** Because `useComputed$()` is synchronous it is not necessary to explicitly track the input signals. +### `useAsyncComputed$()` + +Use `useAsyncComputed$()` to create a computed value that is derived asynchronously. The signal value is the resolved result, not the Promise. + +It behaves more like `useTask$` in that it does not track any signals automatically. You must explicitly track the signals that should cause the async function to re-run, using the `track` function that is passed in the argument. + +If you read the signal value before the async function resolves, it will stop execution of the current function and re-run that function when the computed result is known. + +Therefore you must take care not to read the signal value in the same function that sets it, to avoid re-running the function. Passing the signal value into JSX is safe, because Qwik will know to pause the JSX rendering until the value is known. + ### `useResource$()` Use `useResource$()` to create a computed value that is derived asynchronously. It's the asynchronous version of `useComputed$()`, which includes the `state` of the resource (loading, resolved, rejected) on top of the value. diff --git a/packages/docs/src/routes/docs/(qwikcity)/guides/best-practices/index.mdx b/packages/docs/src/routes/docs/(qwikcity)/guides/best-practices/index.mdx index 64f2b37f132..dff282c24ea 100644 --- a/packages/docs/src/routes/docs/(qwikcity)/guides/best-practices/index.mdx +++ b/packages/docs/src/routes/docs/(qwikcity)/guides/best-practices/index.mdx @@ -98,7 +98,7 @@ Below, only the `useComputed$` function will re-run on any `count.value` change: ```tsx title="Optimal Implementation" export default component$(() => { const count = useSignal(1); - const dobuleCount = useComputed$(() => count.value*2); + const doubleCount = useComputed$(() => count.value*2); return (
{doubleCount.value}
); diff --git a/packages/qwik/src/core/index.ts b/packages/qwik/src/core/index.ts index e40ad6854d0..4074c8bf197 100644 --- a/packages/qwik/src/core/index.ts +++ b/packages/qwik/src/core/index.ts @@ -116,7 +116,16 @@ export type { ResourceProps, ResourceOptions } from './use/use-resource'; export { useResource$, useResourceQrl, Resource } from './use/use-resource'; export { useTask$, useTaskQrl } from './use/use-task'; export { useVisibleTask$, useVisibleTaskQrl } from './use/use-task'; -export { useComputed$, useComputedQrl, createComputed$, createComputedQrl } from './use/use-task'; +export { + useComputed$, + useComputedQrl, + createComputed$, + createComputedQrl, + useAsyncComputed$, + useAsyncComputedQrl, + createAsyncComputed$, + createAsyncComputedQrl, +} from './use/use-task'; export { useErrorBoundary } from './use/use-error-boundary'; export type { ErrorBoundaryStore } from './render/error-handling'; diff --git a/packages/qwik/src/core/qwik.core.api.md b/packages/qwik/src/core/qwik.core.api.md index 290d339428a..4073060216c 100644 --- a/packages/qwik/src/core/qwik.core.api.md +++ b/packages/qwik/src/core/qwik.core.api.md @@ -160,6 +160,16 @@ export interface CorrectedToggleEvent extends Event { readonly prevState: 'open' | 'closed'; } +// Warning: (ae-forgotten-export) The symbol "AsyncComputedFn" needs to be exported by the entry point index.d.ts +// +// @public +export const createAsyncComputed$: (qrl: AsyncComputedFn) => Signal>; + +// Warning: (ae-internal-missing-underscore) The name "createAsyncComputedQrl" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const createAsyncComputedQrl: (qrl: QRL>) => Signal>; + // @public @deprecated export const createComputed$: (qrl: ComputedFn) => Signal>; @@ -1640,6 +1650,14 @@ export const untrack: (fn: () => T) => T; // @public export const unwrapStore: (proxy: T) => T; +// @public +export const useAsyncComputed$: (qrl: AsyncComputedFn) => Signal>; + +// Warning: (ae-internal-missing-underscore) The name "useAsyncComputedQrl" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const useAsyncComputedQrl: (qrl: QRL>) => Signal>; + // @public export const useComputed$: (qrl: ComputedFn) => Signal>; diff --git a/packages/qwik/src/core/use/use-task.ts b/packages/qwik/src/core/use/use-task.ts index 85eca6b64c9..c404bcc5b63 100644 --- a/packages/qwik/src/core/use/use-task.ts +++ b/packages/qwik/src/core/use/use-task.ts @@ -42,6 +42,7 @@ export const TaskFlagsIsResource = 1 << 2; export const TaskFlagsIsComputed = 1 << 3; export const TaskFlagsIsDirty = 1 << 4; export const TaskFlagsIsCleanup = 1 << 5; +export const TaskFlagsIsAsyncComputed = 1 << 6; // // !!DO NOT EDIT THIS COMMENT DIRECTLY!!! @@ -142,6 +143,9 @@ export type TaskFn = (ctx: TaskCtx) => ValueOrPromise void)>; /** @public */ export type ComputedFn = () => T; +/** @public */ +export type AsyncComputedFn = (ctx: TaskCtx) => ValueOrPromise; + /** @public */ export type ResourceFn = (ctx: ResourceCtx) => ValueOrPromise; @@ -337,16 +341,8 @@ export const useComputedQrl = (qrl: QRL>): Signal> = * recalculated, and if the result changed, all tasks which are tracking the signal will be re-run * and all components that read the signal will be re-rendered. * - * The function must be synchronous and must not have any side effects. - * - * Async functions are deprecated because: - * - * - When calculating the first time, it will see it's a promise and it will restart the render - * function. - * - Qwik can't track used signals after the first await, which leads to subtle bugs. - * - Both `useTask$` and `useResource$` are available, without these problems. - * - * In v2, async functions won't work. + * The function must be synchronous and must not have any side effects. If you need a version that + * accepts async functions, use `useAsyncComputed$`. * * @public */ @@ -360,6 +356,60 @@ export const useComputed$ = implicit$FirstArg(useComputedQrl); */ export const createComputed$ = implicit$FirstArg(createComputedQrl); +/** @internal */ +export const createAsyncComputedQrl = (qrl: QRL>): Signal> => { + assertQrl(qrl); + const iCtx = useInvokeContext(); + const hostElement = iCtx.$hostElement$; + const containerState = iCtx.$renderCtx$.$static$.$containerState$; + const elCtx = getContext(hostElement, containerState); + const signal = _createSignal( + undefined as Awaited, + containerState, + SIGNAL_UNASSIGNED | SIGNAL_IMMUTABLE, + undefined + ); + + const task = new Task( + TaskFlagsIsDirty | TaskFlagsIsTask | TaskFlagsIsComputed | TaskFlagsIsAsyncComputed, + // Async computed signals should update immediately + 0, + elCtx.$element$, + qrl, + signal + ); + qrl.$resolveLazy$(containerState.$containerEl$); + (elCtx.$tasks$ ||= []).push(task); + + waitAndRun(iCtx, () => runAsyncComputed(task, containerState, iCtx.$renderCtx$)); + return signal as ReadonlySignal>; +}; + +/** @internal */ +export const useAsyncComputedQrl = (qrl: QRL>): Signal> => { + return useConstant(() => createAsyncComputedQrl(qrl)); +}; + +/** + * Returns a computed signal which is calculated from the given async function. A computed signal is + * a signal which is calculated from other signals. When the signals change, the computed signal is + * recalculated, and if the result changed, all tasks which are tracking the signal will be re-run + * and all components that read the signal will be re-rendered. + * + * The function can be asynchronous and receives a `track` function to observe changes. + * + * @public + */ +export const useAsyncComputed$ = implicit$FirstArg(useAsyncComputedQrl); + +/** + * Returns read-only signal that updates when signals used in the `AsyncComputedFn` change. Unlike + * useAsyncComputed$, this is not a hook and it always creates a new signal. + * + * @public + */ +export const createAsyncComputed$ = implicit$FirstArg(createAsyncComputedQrl); + // // !!DO NOT EDIT THIS COMMENT DIRECTLY!!! // (edit ../readme.md#useTask instead) @@ -510,12 +560,16 @@ export interface ResourceDescriptor export interface ComputedDescriptor extends DescriptorBase, Signal> {} +export interface AsyncComputedDescriptor + extends DescriptorBase, Signal>> {} + export type SubscriberHost = QwikElement; export type SubscriberEffect = | TaskDescriptor | ResourceDescriptor - | ComputedDescriptor; + | ComputedDescriptor + | AsyncComputedDescriptor; export const isResourceTask = (task: SubscriberEffect): task is ResourceDescriptor => { return (task.$flags$ & TaskFlagsIsResource) !== 0; @@ -524,6 +578,12 @@ export const isResourceTask = (task: SubscriberEffect): task is ResourceDescript export const isComputedTask = (task: SubscriberEffect): task is ComputedDescriptor => { return (task.$flags$ & TaskFlagsIsComputed) !== 0; }; + +export const isAsyncComputedTask = ( + task: SubscriberEffect +): task is AsyncComputedDescriptor => { + return (task.$flags$ & TaskFlagsIsAsyncComputed) !== 0; +}; export const runSubscriber = async ( task: SubscriberEffect, containerState: ContainerState, @@ -532,10 +592,12 @@ export const runSubscriber = async ( assertEqual(!!(task.$flags$ & TaskFlagsIsDirty), true, 'Resource is not dirty', task); if (isResourceTask(task)) { return runResource(task, containerState, rCtx); + } else if (isAsyncComputedTask(task)) { + return runAsyncComputed(task as AsyncComputedDescriptor, containerState, rCtx); } else if (isComputedTask(task)) { - return runComputed(task, containerState, rCtx); + return runComputed(task as ComputedDescriptor, containerState, rCtx); } else { - return runTask(task, containerState, rCtx); + return runTask(task as TaskDescriptor, containerState, rCtx); } }; @@ -762,7 +824,7 @@ export const runComputed = ( const result = taskFn(); if (isPromise(result)) { const warningMessage = - 'useComputed$: Async functions in computed tasks are deprecated and will stop working in v2. Use useTask$ or useResource$ instead.'; + 'useComputed$: Async functions in computed tasks are deprecated and will stop working in v2. Use useAsyncComputed$, useTask$ or useResource$ instead.'; const stack = new Error(warningMessage).stack; if (!stack) { logOnceWarn(warningMessage); @@ -781,6 +843,81 @@ export const runComputed = ( } }; +export const runAsyncComputed = ( + task: AsyncComputedDescriptor, + containerState: ContainerState, + rCtx: RenderContext +): ValueOrPromise => { + assertSignal(task.$state$); + task.$flags$ &= ~TaskFlagsIsDirty; + cleanupTask(task); + const hostElement = task.$el$; + const iCtx = newInvokeContext(rCtx.$static$.$locale$, hostElement, undefined, ComputedEvent); + iCtx.$renderCtx$ = rCtx; + + const { $subsManager$: subsManager } = containerState; + const taskFn = task.$qrl$.getFn(iCtx, () => { + subsManager.$clearSub$(task); + }) as AsyncComputedFn; + + const track: Tracker = (obj: (() => unknown) | object | Signal, prop?: string) => { + if (isFunction(obj)) { + const ctx = newInvokeContext(); + ctx.$subscriber$ = [0, task]; + return invoke(ctx, obj); + } + const manager = getSubscriptionManager(obj); + if (manager) { + manager.$addSub$([0, task], prop); + } else { + logErrorAndStop(codeToText(QError_trackUseStore), obj); + } + if (prop) { + return (obj as Record)[prop]; + } else if (isSignal(obj)) { + return obj.value; + } else { + return obj; + } + }; + + const cleanups: (() => void)[] = []; + task.$destroy$ = noSerialize(() => { + cleanups.forEach((fn) => fn()); + }); + + const opts: TaskCtx = { + track, + cleanup(callback) { + cleanups.push(callback); + }, + }; + + const ok = (returnValue: any) => { + untrack(() => { + const signal = task.$state$! as SignalInternal; + signal[QObjectSignalFlags] &= ~SIGNAL_UNASSIGNED; + signal.untrackedValue = noSerialize(returnValue); + signal[QObjectManagerSymbol].$notifySubs$(); + }); + }; + const fail = (reason: unknown) => { + handleError(reason, hostElement, rCtx); + }; + try { + return maybeThen(task.$qrl$.$resolveLazy$(containerState.$containerEl$), () => { + const result = taskFn(opts); + if (isPromise(result)) { + return result.then(ok, fail); + } else { + ok(result); + } + }); + } catch (reason) { + fail(reason); + } +}; + export const cleanupTask = (task: SubscriberEffect) => { const destroy = task.$destroy$; if (destroy) { diff --git a/packages/qwik/src/core/use/use-task.unit.ts b/packages/qwik/src/core/use/use-task.unit.ts index 6074c2c7370..37a4a56910d 100644 --- a/packages/qwik/src/core/use/use-task.unit.ts +++ b/packages/qwik/src/core/use/use-task.unit.ts @@ -3,7 +3,7 @@ import { component$ } from '../component/component.public'; import { useResource$ } from './use-resource'; import { useSignal } from './use-signal'; import { useStore } from './use-store.public'; -import { useTask$ } from './use-task'; +import { useAsyncComputed$, useTask$ } from './use-task'; describe('types', () => { test('track', () => () => { @@ -22,6 +22,13 @@ describe('types', () => { expectTypeOf(track(() => sig.value)).toEqualTypeOf(); expectTypeOf(track(() => store.count)).toEqualTypeOf(); }); + useAsyncComputed$(({ track }) => { + expectTypeOf(track(store)).toEqualTypeOf(store); + expectTypeOf(track(sig)).toEqualTypeOf(); + expectTypeOf(track(() => sig.value)).toEqualTypeOf(); + expectTypeOf(track(() => store.count)).toEqualTypeOf(); + return Promise.resolve(42); + }); return null; }); }); diff --git a/starters/apps/e2e/src/components/signals/signals.tsx b/starters/apps/e2e/src/components/signals/signals.tsx index 4764d82aa1a..952471d933b 100644 --- a/starters/apps/e2e/src/components/signals/signals.tsx +++ b/starters/apps/e2e/src/components/signals/signals.tsx @@ -14,6 +14,7 @@ import { Resource, useComputed$, createComputed$, + useAsyncComputed$, } from "@builder.io/qwik"; import { delay } from "../resource/resource"; import { @@ -138,6 +139,7 @@ export const SignalsChildren = component$(() => { + ); }); @@ -1277,3 +1279,33 @@ export const ManySignals = component$(() => { ); }); + +export const AsyncComputedTest = component$(() => { + const count = useSignal(0); + const store = useStore({ multiplier: 2 }); + + const asyncComputed = useAsyncComputed$(async ({ track }) => { + await new Promise((r) => setTimeout(r, 10)); + const c = track(count); + const m = track(() => store.multiplier); + // Simulate async operation + return Promise.resolve(c * m); + }); + + return ( +
+ + +
Result: {asyncComputed.value}
+
Count: {count.value}
+
Multiplier: {store.multiplier}
+
+ ); +}); diff --git a/starters/e2e/e2e.signals.spec.ts b/starters/e2e/e2e.signals.spec.ts index fec5f7b0f69..9b5017b6ca7 100644 --- a/starters/e2e/e2e.signals.spec.ts +++ b/starters/e2e/e2e.signals.spec.ts @@ -567,6 +567,33 @@ test.describe("signals", () => { await expect(result).toHaveText("1, 1, 1, 1, 1, 1, 1, 1, 1, 1, "); await expect(doubles).toHaveText("2, 2, 2, 2, 2, 2, 2, 2, 2, 2, "); }); + + test("useAsyncComputed$", async ({ page }) => { + const countBtn = page.locator("#async-computed-btn"); + const multiplierBtn = page.locator("#async-computed-multiplier-btn"); + const result = page.locator("#async-computed-result"); + const count = page.locator("#async-computed-count"); + const multiplier = page.locator("#async-computed-multiplier"); + + await expect(result).toHaveText("Result: 0"); + await expect(count).toHaveText("Count: 0"); + await expect(multiplier).toHaveText("Multiplier: 2"); + + await countBtn.click(); + await expect(result).toHaveText("Result: 2"); + await expect(count).toHaveText("Count: 1"); + await expect(multiplier).toHaveText("Multiplier: 2"); + + await multiplierBtn.click(); + await expect(result).toHaveText("Result: 3"); + await expect(count).toHaveText("Count: 1"); + await expect(multiplier).toHaveText("Multiplier: 3"); + + await countBtn.click(); + await expect(result).toHaveText("Result: 6"); + await expect(count).toHaveText("Count: 2"); + await expect(multiplier).toHaveText("Multiplier: 3"); + }); } tests();