diff --git a/.changeset/light-eels-lead.md b/.changeset/light-eels-lead.md new file mode 100644 index 00000000000..74396bc8f4e --- /dev/null +++ b/.changeset/light-eels-lead.md @@ -0,0 +1,6 @@ +--- +'@qwik.dev/core': major +--- + +BREAKING: the `.promise()` method on `useAsync$` now returns a `Promise` instead of `Promise`, to avoid having to put `.catch()` on every call and to promote using the reactive `result.value` and `result.error` properties for handling async results and errors. +- the default serialization strategy for `useAsync$` is now 'always' instead of 'never', because it is likely to be expensive to get. diff --git a/.changeset/rich-peas-invite.md b/.changeset/rich-peas-invite.md new file mode 100644 index 00000000000..4ef56246fa8 --- /dev/null +++ b/.changeset/rich-peas-invite.md @@ -0,0 +1,12 @@ +--- +'@qwik.dev/core': minor +--- + +FEAT: `useAsync$()` now has `interval`, which re-runs the compute function on intervals. You can change signal.interval to enable/disable it, and if you set it during SSR it will automatically resume to do the polling. + This way, you can auto-update data on the client without needing to set up timers or events. For example, you can show a "time ago" string that updates every minute, or you can poll an API for updates, and change the poll interval when the window goes idle. + +- FEAT: `useAsync$()` now has a `concurrency` option, which limits the number of concurrent executions of the compute function. If a new execution is triggered while the limit is reached, it will wait for the previous ones to finish before starting. This is useful for preventing overload when the compute function is expensive or when it involves network requests. The default value is 1, which means that a new execution will wait for the previous one to finish before starting. Setting it to 0 allows unlimited concurrent executions. + In-flight invocations will update the signal value only if they complete before a newer invocation completes. For example, if you have a search input that triggers a new `useAsync$` execution on every keystroke, results will show in the correct order. + +- FEAT: `useAsync$()` now has an `abort()` method, which aborts the current computation and runs cleanups if needed. This allows you to cancel long-running tasks when they are no longer needed, such as when a component unmounts or when a new computation starts. The compute function needs to use the `abortSignal` provided to handle aborts gracefully. + When a new computation starts, the previous computation will be aborted via the abortSignal. This allows you to prevent unnecessary work and ensure that only the latest computation is active. For example, if you have a search input that triggers a new `useAsync$` execution on every keystroke, the previous search will be aborted when a new one starts, ensuring that only the latest search is performed. diff --git a/.changeset/slimy-swans-send.md b/.changeset/slimy-swans-send.md new file mode 100644 index 00000000000..1514b8c4035 --- /dev/null +++ b/.changeset/slimy-swans-send.md @@ -0,0 +1,5 @@ +--- +'@qwik.dev/core': minor +--- + +DEPRECATION: `useResource$` and `` are now deprecated. `useAsync$` is more efficient, more flexible, and easier to use. Use `concurrency: 0` to have the same behavior as `useResource$`. diff --git a/packages/docs/src/components/header/header.css b/packages/docs/src/components/header/header.css index 50084212dae..9781613e974 100644 --- a/packages/docs/src/components/header/header.css +++ b/packages/docs/src/components/header/header.css @@ -17,7 +17,7 @@ .header-container.home-page-header { background: transparent !important; - color: white; + color: black; } .header-open .header-container.home-page-header .menu-toolkit { diff --git a/packages/docs/src/routes/api/qwik/api.json b/packages/docs/src/routes/api/qwik/api.json index 44bc8cb52f7..6a0a238d428 100644 --- a/packages/docs/src/routes/api/qwik/api.json +++ b/packages/docs/src/routes/api/qwik/api.json @@ -220,6 +220,23 @@ "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/qrl/qrl.public.ts", "mdFile": "core._.md" }, + { + "name": "abort", + "id": "asyncsignal-abort", + "hierarchy": [ + { + "name": "AsyncSignal", + "id": "asyncsignal-abort" + }, + { + "name": "abort", + "id": "asyncsignal-abort" + } + ], + "kind": "MethodSignature", + "content": "Abort the current computation and run cleanups if needed.\n\n\n```typescript\nabort(reason?: any): void;\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nreason\n\n\n\n\nany\n\n\n\n\n_(Optional)_\n\n\n
\n\n**Returns:**\n\nvoid", + "mdFile": "core.asyncsignal.abort.md" + }, { "name": "AsyncFn", "id": "asyncfn", @@ -230,7 +247,7 @@ } ], "kind": "TypeAlias", - "content": "```typescript\nexport type AsyncFn = (ctx: AsyncCtx) => Promise;\n```", + "content": "Note, we don't pass the generic type to AsyncCtx because it causes TypeScript to not infer the type of the resource correctly. The type is only used for the `previous` property, which is not commonly used, and can be easily cast if needed.\n\n\n```typescript\nexport type AsyncFn = (ctx: AsyncCtx) => ValueOrPromise;\n```\n**References:** [ValueOrPromise](#valueorpromise)", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-async.ts", "mdFile": "core.asyncfn.md" }, @@ -244,10 +261,24 @@ } ], "kind": "Interface", - "content": "```typescript\nexport interface AsyncSignal extends ComputedSignal \n```\n**Extends:** [ComputedSignal](#computedsignal)<T>\n\n\n\n\n\n
\n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\n[error](#)\n\n\n\n\n\n\n\nError \\| undefined\n\n\n\n\nThe error that occurred while computing the signal.\n\n\n
\n\n[loading](#)\n\n\n\n\n\n\n\nboolean\n\n\n\n\nWhether the signal is currently loading.\n\n\n
\n\n\n\n\n
\n\nMethod\n\n\n\n\nDescription\n\n\n
\n\n[promise()](#asyncsignal-promise)\n\n\n\n\nA promise that resolves when the value is computed.\n\n\n
", + "content": "```typescript\nexport interface AsyncSignal extends ComputedSignal \n```\n**Extends:** [ComputedSignal](#computedsignal)<T>\n\n\n\n\n\n\n
\n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\n[error](#)\n\n\n\n\n\n\n\nError \\| undefined\n\n\n\n\nThe error that occurred while computing the signal, if any. This will be cleared when the signal is successfully computed.\n\n\n
\n\n[interval](#)\n\n\n\n\n\n\n\nnumber\n\n\n\n\nPoll interval in ms. Writable and immediately effective when the signal has consumers. If set to `0`, polling stops.\n\n\n
\n\n[loading](#)\n\n\n\n\n\n\n\nboolean\n\n\n\n\nWhether the signal is currently loading. This will trigger lazy loading of the signal, so you can use it like this:\n\n```tsx\nsignal.loading ? : signal.error ? : \n```\n\n\n
\n\n\n\n\n\n
\n\nMethod\n\n\n\n\nDescription\n\n\n
\n\n[abort(reason)](#asyncsignal-abort)\n\n\n\n\nAbort the current computation and run cleanups if needed.\n\n\n
\n\n[promise()](#asyncsignal-promise)\n\n\n\n\nA promise that resolves when the value is computed or rejected.\n\n\n
", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/reactive-primitives/signal.public.ts", "mdFile": "core.asyncsignal.md" }, + { + "name": "AsyncSignalOptions", + "id": "asyncsignaloptions", + "hierarchy": [ + { + "name": "AsyncSignalOptions", + "id": "asyncsignaloptions" + } + ], + "kind": "Interface", + "content": "```typescript\nexport interface AsyncSignalOptions extends ComputedOptions \n```\n**Extends:** [ComputedOptions](#computedoptions)\n\n\n\n\n\n\n\n\n\n
\n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\n[awaitPrevious?](#)\n\n\n\n\n\n\n\nboolean\n\n\n\n\n_(Optional)_ Wait for previous invocation to complete before running again.\n\nDefaults to `true`.\n\n\n
\n\n[concurrency?](#)\n\n\n\n\n\n\n\nnumber\n\n\n\n\n_(Optional)_ Maximum number of concurrent computations. Use `0` for unlimited.\n\nDefaults to `1`.\n\n\n
\n\n[eagerCleanup?](#)\n\n\n\n\n\n\n\nboolean\n\n\n\n\n_(Optional)_ When subscribers drop to 0, run cleanup in the next tick, instead of waiting for the function inputs to change.\n\nDefaults to `false`, meaning cleanup happens only when inputs change.\n\n\n
\n\n[initial?](#)\n\n\n\n\n\n\n\nT \\| (() => T)\n\n\n\n\n_(Optional)_ Like useSignal's `initial`; prevents the throw on first read when uninitialized\n\n\n
\n\n[interval?](#)\n\n\n\n\n\n\n\nnumber\n\n\n\n\n_(Optional)_ In the browser, re-run the function after `interval` ms if subscribers exist, even when no input state changed. If `0`, does not poll.\n\nDefaults to `0`.\n\n\n
\n\n[timeout?](#)\n\n\n\n\n\n\n\nnumber\n\n\n\n\n_(Optional)_ Maximum time in milliseconds to wait for the async computation to complete. If exceeded, the computation is aborted and an error is thrown.\n\nIf `0`, no timeout is applied.\n\nDefaults to `0`.\n\n\n
", + "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/reactive-primitives/types.ts", + "mdFile": "core.asyncsignaloptions.md" + }, { "name": "cache", "id": "resourcectx-cache", @@ -262,7 +293,7 @@ } ], "kind": "MethodSignature", - "content": "```typescript\ncache(policyOrMilliseconds: number | 'immutable'): void;\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\npolicyOrMilliseconds\n\n\n\n\nnumber \\| 'immutable'\n\n\n\n\n\n
\n\n**Returns:**\n\nvoid", + "content": "> Warning: This API is now obsolete.\n> \n> Does not do anything\n> \n\n\n```typescript\ncache(policyOrMilliseconds: number | 'immutable'): void;\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\npolicyOrMilliseconds\n\n\n\n\nnumber \\| 'immutable'\n\n\n\n\n\n
\n\n**Returns:**\n\nvoid", "mdFile": "core.resourcectx.cache.md" }, { @@ -446,7 +477,7 @@ } ], "kind": "Function", - "content": "Create an async computed signal which is calculated from the given QRL. A computed signal is a signal which is calculated from other signals or async operation. When the signals change, the computed signal is recalculated.\n\nThe QRL must be a function which returns the value of the signal. The function must not have side effects, and it can be async.\n\n\n```typescript\ncreateAsync$: (qrl: () => Promise, options?: ComputedOptions) => AsyncSignal\n```\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() => Promise<T>\n\n\n\n\n\n
\n\noptions\n\n\n\n\n[ComputedOptions](#computedoptions)\n\n\n\n\n_(Optional)_\n\n\n
\n\n**Returns:**\n\n[AsyncSignal](#asyncsignal)<T>", + "content": "Create a signal holding a `.value` which is calculated from the given async function (QRL). The standalone version of `useAsync$`.\n\n\n```typescript\ncreateAsync$: (qrl: (arg: AsyncCtx) => Promise, options?: AsyncSignalOptions) => AsyncSignal\n```\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(arg: AsyncCtx<T>) => Promise<T>\n\n\n\n\n\n
\n\noptions\n\n\n\n\n[AsyncSignalOptions](#asyncsignaloptions)<T>\n\n\n\n\n_(Optional)_\n\n\n
\n\n**Returns:**\n\n[AsyncSignal](#asyncsignal)<T>", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/reactive-primitives/signal.public.ts", "mdFile": "core.createasync_.md" }, @@ -460,7 +491,7 @@ } ], "kind": "Function", - "content": "Create a computed signal which is calculated from the given QRL. A computed signal is a signal which is calculated from other signals. When the signals change, the computed signal is recalculated.\n\nThe QRL must be a function which returns the value of the signal. The function must not have side effects, and it must be synchronous.\n\nIf you need the function to be async, use `useAsync$` instead.\n\n\n```typescript\ncreateComputed$: (qrl: () => T, options?: ComputedOptions) => ComputedReturnType\n```\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() => T\n\n\n\n\n\n
\n\noptions\n\n\n\n\n[ComputedOptions](#computedoptions)\n\n\n\n\n_(Optional)_\n\n\n
\n\n**Returns:**\n\n[ComputedReturnType](#computedreturntype)<T>", + "content": "Create a computed signal which is calculated from the given QRL. A computed signal is a signal which is calculated from other signals. When the signals change, the computed signal is recalculated.\n\nThe QRL must be a function which returns the value of the signal. The function must not have side effects, and it must be synchronous.\n\nIf you need the function to be async, use `createAsync$` instead (don't forget to use `track()`).\n\n\n```typescript\ncreateComputed$: (qrl: () => T, options?: ComputedOptions) => ComputedReturnType\n```\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() => T\n\n\n\n\n\n
\n\noptions\n\n\n\n\n[ComputedOptions](#computedoptions)\n\n\n\n\n_(Optional)_\n\n\n
\n\n**Returns:**\n\n[ComputedReturnType](#computedreturntype)<T>", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/reactive-primitives/signal.public.ts", "mdFile": "core.createcomputed_.md" }, @@ -1255,7 +1286,7 @@ } ], "kind": "MethodSignature", - "content": "A promise that resolves when the value is computed.\n\n\n```typescript\npromise(): Promise;\n```\n**Returns:**\n\nPromise<T>", + "content": "A promise that resolves when the value is computed or rejected.\n\n\n```typescript\npromise(): Promise;\n```\n**Returns:**\n\nPromise<void>", "mdFile": "core.asyncsignal.promise.md" }, { @@ -1814,7 +1845,7 @@ } ], "kind": "Function", - "content": "This method works like an async memoized function that runs whenever some tracked value changes and returns some data.\n\n`useResource` however returns immediate a `ResourceReturn` object that contains the data and a state that indicates if the data is available or not.\n\nThe status can be one of the following:\n\n- `pending` - the data is not yet available. - `resolved` - the data is available. - `rejected` - the data is not available due to an error or timeout.\n\nBe careful when using a `try/catch` statement in `useResource$`. If you catch the error and don't re-throw it (or a new Error), the resource status will never be `rejected`.\n\n\\#\\#\\# Example\n\nExample showing how `useResource` to perform a fetch to request the weather, whenever the input city name changes.\n\n```tsx\nconst Cmp = component$(() => {\n const cityS = useSignal('');\n\n const weatherResource = useResource$(async ({ track, cleanup }) => {\n const cityName = track(cityS);\n const abortController = new AbortController();\n cleanup(() => abortController.abort('cleanup'));\n const res = await fetch(`http://weatherdata.com?city=${cityName}`, {\n signal: abortController.signal,\n });\n const data = await res.json();\n return data as { temp: number };\n });\n\n return (\n
\n \n {\n return
Temperature: {weather.temp}
;\n }}\n />\n
\n );\n});\n```\n\n\n```typescript\nResource: (props: ResourceProps) => JSXOutput\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nprops\n\n\n\n\n[ResourceProps](#resourceprops)<T>\n\n\n\n\n\n
\n\n**Returns:**\n\n[JSXOutput](#jsxoutput)", + "content": "> Warning: This API is now obsolete.\n> \n> Use `useAsync$` instead, which is more efficient, and has a more flexible API. Just read the `loading` and `error` properties from the returned signal to determine the status.\n> \n\n```tsx\nconst Cmp = component$(() => {\n const city = useSignal('');\n\n const weather = useAsync$(async ({ track, cleanup, abortSignal }) => {\n const cityName = track(city);\n const res = await fetch(`http://weatherdata.com?city=${cityName}`, {\n signal: abortSignal,\n });\n const temp = (await res.json()) as { temp: number };\n return temp;\n });\n\n return (\n
\n \n
\n Temperature:{' '}\n {weather.loading\n ? 'Loading...'\n : weather.error\n ? `Error: ${weather.error.message}`\n : weather.value.temp}\n
\n
\n );\n});\n```\n\n\n```typescript\nResource: ({ value, onResolved, onPending, onRejected, }: ResourceProps) => JSXOutput\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\n{ value, onResolved, onPending, onRejected, }\n\n\n\n\n[ResourceProps](#resourceprops)<T>\n\n\n\n\n\n
\n\n**Returns:**\n\n[JSXOutput](#jsxoutput)", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-resource.ts", "mdFile": "core.resource.md" }, @@ -1828,7 +1859,7 @@ } ], "kind": "Interface", - "content": "```typescript\nexport interface ResourceCtx \n```\n\n\n\n\n\n
\n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\n[previous](#)\n\n\n\n\n`readonly`\n\n\n\n\nT \\| undefined\n\n\n\n\n\n
\n\n[track](#)\n\n\n\n\n`readonly`\n\n\n\n\n[Tracker](#tracker)\n\n\n\n\n\n
\n\n\n\n\n\n
\n\nMethod\n\n\n\n\nDescription\n\n\n
\n\n[cache(policyOrMilliseconds)](#resourcectx-cache)\n\n\n\n\n\n
\n\n[cleanup(callback)](#)\n\n\n\n\n\n
", + "content": "```typescript\nexport interface ResourceCtx extends AsyncCtx \n```\n**Extends:** AsyncCtx<T>\n\n\n\n\n
\n\nMethod\n\n\n\n\nDescription\n\n\n
\n\n[cache(policyOrMilliseconds)](#resourcectx-cache)\n\n\n\n\n\n
", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-resource.ts", "mdFile": "core.resourcectx.md" }, @@ -1842,7 +1873,7 @@ } ], "kind": "TypeAlias", - "content": "```typescript\nexport type ResourceFn = (ctx: ResourceCtx) => ValueOrPromise;\n```\n**References:** [ResourceCtx](#resourcectx), [ValueOrPromise](#valueorpromise)", + "content": "```typescript\nexport type ResourceFn = (ctx: ResourceCtx) => ValueOrPromise;\n```\n**References:** [ResourceCtx](#resourcectx), [ValueOrPromise](#valueorpromise)", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-resource.ts", "mdFile": "core.resourcefn.md" }, @@ -1869,8 +1900,8 @@ "id": "resourcepending" } ], - "kind": "Interface", - "content": "```typescript\nexport interface ResourcePending \n```\n\n\n\n\n\n
\n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\n[loading](#)\n\n\n\n\n`readonly`\n\n\n\n\nboolean\n\n\n\n\n\n
\n\n[value](#)\n\n\n\n\n`readonly`\n\n\n\n\nPromise<T>\n\n\n\n\n\n
", + "kind": "TypeAlias", + "content": "```typescript\nexport type ResourcePending = ResourceReturn;\n```\n**References:** [ResourceReturn](#resourcereturn)", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-resource.ts", "mdFile": "core.resourcepending.md" }, @@ -1897,8 +1928,8 @@ "id": "resourcerejected" } ], - "kind": "Interface", - "content": "```typescript\nexport interface ResourceRejected \n```\n\n\n\n\n\n
\n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\n[loading](#)\n\n\n\n\n`readonly`\n\n\n\n\nboolean\n\n\n\n\n\n
\n\n[value](#)\n\n\n\n\n`readonly`\n\n\n\n\nPromise<T>\n\n\n\n\n\n
", + "kind": "TypeAlias", + "content": "```typescript\nexport type ResourceRejected = ResourceReturn;\n```\n**References:** [ResourceReturn](#resourcereturn)", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-resource.ts", "mdFile": "core.resourcerejected.md" }, @@ -1911,8 +1942,8 @@ "id": "resourceresolved" } ], - "kind": "Interface", - "content": "```typescript\nexport interface ResourceResolved \n```\n\n\n\n\n\n
\n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\n[loading](#)\n\n\n\n\n`readonly`\n\n\n\n\nboolean\n\n\n\n\n\n
\n\n[value](#)\n\n\n\n\n`readonly`\n\n\n\n\nPromise<T>\n\n\n\n\n\n
", + "kind": "TypeAlias", + "content": "```typescript\nexport type ResourceResolved = ResourceReturn;\n```\n**References:** [ResourceReturn](#resourcereturn)", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-resource.ts", "mdFile": "core.resourceresolved.md" }, @@ -1926,7 +1957,7 @@ } ], "kind": "TypeAlias", - "content": "```typescript\nexport type ResourceReturn = ResourcePending | ResourceResolved | ResourceRejected;\n```\n**References:** [ResourcePending](#resourcepending), [ResourceResolved](#resourceresolved), [ResourceRejected](#resourcerejected)", + "content": "```typescript\nexport type ResourceReturn = {\n readonly value: Promise;\n readonly loading: boolean;\n};\n```", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-resource.ts", "mdFile": "core.resourcereturn.md" }, @@ -1940,7 +1971,7 @@ } ], "kind": "TypeAlias", - "content": "```typescript\nexport type SerializationStrategy = 'never' | 'always';\n```", + "content": "Serialization strategy for computed and async signals. This determines whether to serialize their value during SSR.\n\n- `never`: The value is never serialized. When the component is resumed, the value will be recalculated when it is first read. - `always`: The value is always serialized. This is the default.\n\n\\*\\*IMPORTANT\\*\\*: When you use `never`, your serialized HTML is smaller, but the recalculation will trigger subscriptions, meaning that other signals using this signal will recalculate, even if this signal didn't change.\n\nThis is normally not a problem, but for async signals it may mean fetching something again.\n\n\n```typescript\nexport type SerializationStrategy = 'never' | 'always';\n```", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/types.ts", "mdFile": "core.serializationstrategy.md" }, @@ -2346,7 +2377,7 @@ } ], "kind": "Function", - "content": "Creates an AsyncSignal which holds the result of the given async function. If the function uses `track()` to track reactive state, and that state changes, the AsyncSignal is recalculated, and if the result changed, all tasks which are tracking the AsyncSignal will be re-run and all subscribers (components, tasks etc) that read the AsyncSignal will be updated.\n\nIf the async function throws an error, the AsyncSignal will capture the error and set the `error` property. The error can be cleared by re-running the async function successfully.\n\nWhile the async function is running, the `loading` property will be set to `true`. Once the function completes, `loading` will be set to `false`.\n\nIf the value has not yet been resolved, reading the AsyncSignal will throw a Promise, which will retry the component or task once the value resolves.\n\nIf the value has been resolved, but the async function is re-running, reading the AsyncSignal will subscribe to it and return the last resolved value until the new value is ready. As soon as the new value is ready, the subscribers will be updated.\n\n\n```typescript\nuseAsync$: (qrl: AsyncFn, options?: ComputedOptions | undefined) => AsyncSignal\n```\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[AsyncFn](#asyncfn)<T>\n\n\n\n\n\n
\n\noptions\n\n\n\n\n[ComputedOptions](#computedoptions) \\| undefined\n\n\n\n\n_(Optional)_\n\n\n
\n\n**Returns:**\n\n[AsyncSignal](#asyncsignal)<T>", + "content": "Creates an AsyncSignal which holds the result of the given async function. If the function uses `track()` to track reactive state, and that state changes, the AsyncSignal is recalculated, and if the result changed, all tasks which are tracking the AsyncSignal will be re-run and all subscribers (components, tasks etc) that read the AsyncSignal will be updated.\n\nIf the async function throws an error, the AsyncSignal will capture the error and set the `error` property. The error can be cleared by re-running the async function successfully.\n\nWhile the async function is running, the `loading` property will be set to `true`. Once the function completes, `loading` will be set to `false`.\n\nIf the value has not yet been resolved, reading the AsyncSignal will throw a Promise, which will retry the component or task once the value resolves.\n\nIf the value has been resolved, but the async function is re-running, reading the AsyncSignal will subscribe to it and return the last resolved value until the new value is ready. As soon as the new value is ready, the subscribers will be updated.\n\n\n```typescript\nuseAsync$: (qrl: AsyncFn, options?: AsyncSignalOptions | undefined) => AsyncSignal\n```\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[AsyncFn](#asyncfn)<T>\n\n\n\n\n\n
\n\noptions\n\n\n\n\n[AsyncSignalOptions](#asyncsignaloptions)<T> \\| undefined\n\n\n\n\n_(Optional)_\n\n\n
\n\n**Returns:**\n\n[AsyncSignal](#asyncsignal)<T>", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-async.ts", "mdFile": "core.useasync_.md" }, @@ -2486,7 +2517,7 @@ } ], "kind": "Function", - "content": "This method works like an async memoized function that runs whenever some tracked value changes and returns some data.\n\n`useResource` however returns immediate a `ResourceReturn` object that contains the data and a state that indicates if the data is available or not.\n\nThe status can be one of the following:\n\n- `pending` - the data is not yet available. - `resolved` - the data is available. - `rejected` - the data is not available due to an error or timeout.\n\nBe careful when using a `try/catch` statement in `useResource$`. If you catch the error and don't re-throw it (or a new Error), the resource status will never be `rejected`.\n\n\\#\\#\\# Example\n\nExample showing how `useResource` to perform a fetch to request the weather, whenever the input city name changes.\n\n```tsx\nconst Cmp = component$(() => {\n const cityS = useSignal('');\n\n const weatherResource = useResource$(async ({ track, cleanup }) => {\n const cityName = track(cityS);\n const abortController = new AbortController();\n cleanup(() => abortController.abort('cleanup'));\n const res = await fetch(`http://weatherdata.com?city=${cityName}`, {\n signal: abortController.signal,\n });\n const data = await res.json();\n return data as { temp: number };\n });\n\n return (\n
\n \n {\n return
Temperature: {weather.temp}
;\n }}\n />\n
\n );\n});\n```\n\n\n```typescript\nuseResource$: (generatorFn: ResourceFn, opts?: ResourceOptions) => ResourceReturn\n```\n\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\ngeneratorFn\n\n\n\n\n[ResourceFn](#resourcefn)<T>\n\n\n\n\n\n
\n\nopts\n\n\n\n\n[ResourceOptions](#resourceoptions)\n\n\n\n\n_(Optional)_\n\n\n
\n\n**Returns:**\n\n[ResourceReturn](#resourcereturn)<T>", + "content": "> Warning: This API is now obsolete.\n> \n> Use `useAsync$` instead, which is more powerful and flexible. `useResource$` is still available for backward compatibility but it is recommended to migrate to `useAsync$` for new code and when updating existing code.\n> \n\nThis method works like an async memoized function that runs whenever some tracked value changes and returns some data.\n\n`useResource` however returns immediate a `ResourceReturn` object that contains the data and a state that indicates if the data is available or not.\n\nThe status can be one of the following:\n\n- `pending` - the data is not yet available. - `resolved` - the data is available. - `rejected` - the data is not available due to an error or timeout.\n\nBe careful when using a `try/catch` statement in `useResource$`. If you catch the error and don't re-throw it (or a new Error), the resource status will never be `rejected`.\n\n\n```typescript\nuseResource$: (qrl: import(\"./use-resource\").ResourceFn, opts?: import(\"./use-resource\").ResourceOptions | undefined) => import(\"./use-resource\").ResourceReturn\n```\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\nimport(\"./use-resource\").[ResourceFn](#resourcefn)<T>\n\n\n\n\n\n
\n\nopts\n\n\n\n\nimport(\"./use-resource\").[ResourceOptions](#resourceoptions) \\| undefined\n\n\n\n\n_(Optional)_\n\n\n
\n\n**Returns:**\n\nimport(\"./use-resource\").[ResourceReturn](#resourcereturn)<T>", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-resource-dollar.ts", "mdFile": "core.useresource_.md" }, diff --git a/packages/docs/src/routes/api/qwik/index.mdx b/packages/docs/src/routes/api/qwik/index.mdx index 25f183b258f..27e241ef970 100644 --- a/packages/docs/src/routes/api/qwik/index.mdx +++ b/packages/docs/src/routes/api/qwik/index.mdx @@ -120,12 +120,56 @@ Expression which should be lazy loaded [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/qrl/qrl.public.ts) +## abort + +Abort the current computation and run cleanups if needed. + +```typescript +abort(reason?: any): void; +``` + + + +
+ +Parameter + + + +Type + + + +Description + +
+ +reason + + + +any + + + +_(Optional)_ + +
+ +**Returns:** + +void + ## AsyncFn +Note, we don't pass the generic type to AsyncCtx because it causes TypeScript to not infer the type of the resource correctly. The type is only used for the `previous` property, which is not commonly used, and can be easily cast if needed. + ```typescript -export type AsyncFn = (ctx: AsyncCtx) => Promise; +export type AsyncFn = (ctx: AsyncCtx) => ValueOrPromise; ``` +**References:** [ValueOrPromise](#valueorpromise) + [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-async.ts) ## AsyncSignal @@ -165,7 +209,22 @@ Error \| undefined -The error that occurred while computing the signal. +The error that occurred while computing the signal, if any. This will be cleared when the signal is successfully computed. + + + + +[interval](#) + + + + + +number + + + +Poll interval in ms. Writable and immediately effective when the signal has consumers. If set to `0`, polling stops. @@ -180,7 +239,17 @@ boolean -Whether the signal is currently loading. +Whether the signal is currently loading. This will trigger lazy loading of the signal, so you can use it like this: + +```tsx +signal.loading ? ( + +) : signal.error ? ( + +) : ( + +); +``` @@ -196,19 +265,163 @@ Description +[abort(reason)](#asyncsignal-abort) + + + +Abort the current computation and run cleanups if needed. + + + + [promise()](#asyncsignal-promise) -A promise that resolves when the value is computed. +A promise that resolves when the value is computed or rejected. [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/reactive-primitives/signal.public.ts) +## AsyncSignalOptions + +```typescript +export interface AsyncSignalOptions extends ComputedOptions +``` + +**Extends:** [ComputedOptions](#computedoptions) + + + + + + + + +
+ +Property + + + +Modifiers + + + +Type + + + +Description + +
+ +[awaitPrevious?](#) + + + + + +boolean + + + +_(Optional)_ Wait for previous invocation to complete before running again. + +Defaults to `true`. + +
+ +[concurrency?](#) + + + + + +number + + + +_(Optional)_ Maximum number of concurrent computations. Use `0` for unlimited. + +Defaults to `1`. + +
+ +[eagerCleanup?](#) + + + + + +boolean + + + +_(Optional)_ When subscribers drop to 0, run cleanup in the next tick, instead of waiting for the function inputs to change. + +Defaults to `false`, meaning cleanup happens only when inputs change. + +
+ +[initial?](#) + + + + + +T \| (() => T) + + + +_(Optional)_ Like useSignal's `initial`; prevents the throw on first read when uninitialized + +
+ +[interval?](#) + + + + + +number + + + +_(Optional)_ In the browser, re-run the function after `interval` ms if subscribers exist, even when no input state changed. If `0`, does not poll. + +Defaults to `0`. + +
+ +[timeout?](#) + + + + + +number + + + +_(Optional)_ Maximum time in milliseconds to wait for the async computation to complete. If exceeded, the computation is aborted and an error is thrown. + +If `0`, no timeout is applied. + +Defaults to `0`. + +
+ +[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/reactive-primitives/types.ts) + ## cache +> Warning: This API is now obsolete. +> +> Does not do anything + ```typescript cache(policyOrMilliseconds: number | 'immutable'): void; ``` @@ -819,13 +1032,13 @@ Description ## createAsync$ -Create an async computed signal which is calculated from the given QRL. A computed signal is a signal which is calculated from other signals or async operation. When the signals change, the computed signal is recalculated. - -The QRL must be a function which returns the value of the signal. The function must not have side effects, and it can be async. +Create a signal holding a `.value` which is calculated from the given async function (QRL). The standalone version of `useAsync$`. ```typescript -createAsync$: (qrl: () => Promise, options?: ComputedOptions) => - AsyncSignal; +createAsync$: ( + qrl: (arg: AsyncCtx) => Promise, + options?: AsyncSignalOptions, +) => AsyncSignal; ```
@@ -847,7 +1060,7 @@ qrl -() => Promise<T> +(arg: AsyncCtx<T>) => Promise<T> @@ -858,7 +1071,7 @@ options -[ComputedOptions](#computedoptions) +[AsyncSignalOptions](#asyncsignaloptions)<T> @@ -879,7 +1092,7 @@ Create a computed signal which is calculated from the given QRL. A computed sign The QRL must be a function which returns the value of the signal. The function must not have side effects, and it must be synchronous. -If you need the function to be async, use `useAsync$` instead. +If you need the function to be async, use `createAsync$` instead (don't forget to use `track()`). ```typescript createComputed$: (qrl: () => T, options?: ComputedOptions) => @@ -2618,15 +2831,15 @@ opts ## promise -A promise that resolves when the value is computed. +A promise that resolves when the value is computed or rejected. ```typescript -promise(): Promise; +promise(): Promise; ``` **Returns:** -Promise<T> +Promise<void> ## PropFunction @@ -3699,51 +3912,42 @@ StreamWriter ## Resource -This method works like an async memoized function that runs whenever some tracked value changes and returns some data. - -`useResource` however returns immediate a `ResourceReturn` object that contains the data and a state that indicates if the data is available or not. - -The status can be one of the following: - -- `pending` - the data is not yet available. - `resolved` - the data is available. - `rejected` - the data is not available due to an error or timeout. - -Be careful when using a `try/catch` statement in `useResource$`. If you catch the error and don't re-throw it (or a new Error), the resource status will never be `rejected`. - -### Example - -Example showing how `useResource` to perform a fetch to request the weather, whenever the input city name changes. +> Warning: This API is now obsolete. +> +> Use `useAsync$` instead, which is more efficient, and has a more flexible API. Just read the `loading` and `error` properties from the returned signal to determine the status. ```tsx const Cmp = component$(() => { - const cityS = useSignal(""); + const city = useSignal(""); - const weatherResource = useResource$(async ({ track, cleanup }) => { - const cityName = track(cityS); - const abortController = new AbortController(); - cleanup(() => abortController.abort("cleanup")); + const weather = useAsync$(async ({ track, cleanup, abortSignal }) => { + const cityName = track(city); const res = await fetch(`http://weatherdata.com?city=${cityName}`, { - signal: abortController.signal, + signal: abortSignal, }); - const data = await res.json(); - return data as { temp: number }; + const temp = (await res.json()) as { temp: number }; + return temp; }); return (
- - { - return
Temperature: {weather.temp}
; - }} - /> + +
+ Temperature:{" "} + {weather.loading + ? "Loading..." + : weather.error + ? `Error: ${weather.error.message}` + : weather.value.temp} +
); }); ``` ```typescript -Resource: (props: ResourceProps) => JSXOutput; +Resource: ({ value, onResolved, onPending, onRejected }: ResourceProps) => + JSXOutput; ```
@@ -3761,7 +3965,7 @@ Description
-props +\{ value, onResolved, onPending, onRejected, } @@ -3781,57 +3985,10 @@ props ## ResourceCtx ```typescript -export interface ResourceCtx +export interface ResourceCtx extends AsyncCtx ``` - - - -
- -Property - - - -Modifiers - - - -Type - - - -Description - -
- -[previous](#) - - - -`readonly` - - - -T \| undefined - - - -
- -[track](#) - - - -`readonly` - - - -[Tracker](#tracker) - - - -
+**Extends:** AsyncCtx<T> -
@@ -3848,13 +4005,6 @@ Description -
- -[cleanup(callback)](#) - - -
@@ -3863,7 +4013,7 @@ Description ## ResourceFn ```typescript -export type ResourceFn = (ctx: ResourceCtx) => ValueOrPromise; +export type ResourceFn = (ctx: ResourceCtx) => ValueOrPromise; ``` **References:** [ResourceCtx](#resourcectx), [ValueOrPromise](#valueorpromise) @@ -3917,57 +4067,10 @@ _(Optional)_ Timeout in milliseconds. If the resource takes more than the specif ## ResourcePending ```typescript -export interface ResourcePending +export type ResourcePending = ResourceReturn; ``` - - - -
- -Property - - - -Modifiers - - - -Type - - - -Description - -
- -[loading](#) - - - -`readonly` - - - -boolean - - - -
- -[value](#) - - - -`readonly` - - - -Promise<T> - - - -
+**References:** [ResourceReturn](#resourcereturn) [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-resource.ts) @@ -4059,132 +4162,44 @@ _(Optional)_ ## ResourceRejected ```typescript -export interface ResourceRejected +export type ResourceRejected = ResourceReturn; ``` - - - -
- -Property - - - -Modifiers - - - -Type - - - -Description - -
- -[loading](#) - - - -`readonly` - - - -boolean - - - -
- -[value](#) - - - -`readonly` - - - -Promise<T> - - - -
+**References:** [ResourceReturn](#resourcereturn) [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-resource.ts) ## ResourceResolved ```typescript -export interface ResourceResolved +export type ResourceResolved = ResourceReturn; ``` - - - -
- -Property - - - -Modifiers - - - -Type - - - -Description - -
- -[loading](#) - - - -`readonly` - - - -boolean - - - -
- -[value](#) - - - -`readonly` - - - -Promise<T> - - - -
+**References:** [ResourceReturn](#resourcereturn) [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-resource.ts) ## ResourceReturn ```typescript -export type ResourceReturn = - | ResourcePending - | ResourceResolved - | ResourceRejected; +export type ResourceReturn = { + readonly value: Promise; + readonly loading: boolean; +}; ``` -**References:** [ResourcePending](#resourcepending), [ResourceResolved](#resourceresolved), [ResourceRejected](#resourcerejected) - [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-resource.ts) ## SerializationStrategy +Serialization strategy for computed and async signals. This determines whether to serialize their value during SSR. + +- `never`: The value is never serialized. When the component is resumed, the value will be recalculated when it is first read. - `always`: The value is always serialized. This is the default. + +\*\*IMPORTANT\*\*: When you use `never`, your serialized HTML is smaller, but the recalculation will trigger subscriptions, meaning that other signals using this signal will recalculate, even if this signal didn't change. + +This is normally not a problem, but for async signals it may mean fetching something again. + ```typescript export type SerializationStrategy = "never" | "always"; ``` @@ -9012,7 +9027,7 @@ If the value has not yet been resolved, reading the AsyncSignal will throw a Pro If the value has been resolved, but the async function is re-running, reading the AsyncSignal will subscribe to it and return the last resolved value until the new value is ready. As soon as the new value is ready, the subscribers will be updated. ```typescript -useAsync$: (qrl: AsyncFn, options?: ComputedOptions | undefined) => +useAsync$: (qrl: AsyncFn, options?: AsyncSignalOptions | undefined) => AsyncSignal; ``` @@ -9046,7 +9061,7 @@ options
-[ComputedOptions](#computedoptions) \| undefined +[AsyncSignalOptions](#asyncsignaloptions)<T> \| undefined @@ -9511,6 +9526,10 @@ void ## useResource$ +> Warning: This API is now obsolete. +> +> Use `useAsync$` instead, which is more powerful and flexible. `useResource$` is still available for backward compatibility but it is recommended to migrate to `useAsync$` for new code and when updating existing code. + This method works like an async memoized function that runs whenever some tracked value changes and returns some data. `useResource` however returns immediate a `ResourceReturn` object that contains the data and a state that indicates if the data is available or not. @@ -9521,42 +9540,11 @@ The status can be one of the following: Be careful when using a `try/catch` statement in `useResource$`. If you catch the error and don't re-throw it (or a new Error), the resource status will never be `rejected`. -### Example - -Example showing how `useResource` to perform a fetch to request the weather, whenever the input city name changes. - -```tsx -const Cmp = component$(() => { - const cityS = useSignal(""); - - const weatherResource = useResource$(async ({ track, cleanup }) => { - const cityName = track(cityS); - const abortController = new AbortController(); - cleanup(() => abortController.abort("cleanup")); - const res = await fetch(`http://weatherdata.com?city=${cityName}`, { - signal: abortController.signal, - }); - const data = await res.json(); - return data as { temp: number }; - }); - - return ( -
- - { - return
Temperature: {weather.temp}
; - }} - /> -
- ); -}); -``` - ```typescript -useResource$: (generatorFn: ResourceFn, opts?: ResourceOptions) => - ResourceReturn; +useResource$: ( + qrl: import("./use-resource").ResourceFn, + opts?: import("./use-resource").ResourceOptions | undefined, +) => import("./use-resource").ResourceReturn; ```
@@ -9574,11 +9562,11 @@ Description
-generatorFn +qrl -[ResourceFn](#resourcefn)<T> +import("./use-resource").[ResourceFn](#resourcefn)<T> @@ -9589,7 +9577,7 @@ opts -[ResourceOptions](#resourceoptions) +import("./use-resource").[ResourceOptions](#resourceoptions) \| undefined @@ -9600,7 +9588,7 @@ _(Optional)_ **Returns:** -[ResourceReturn](#resourcereturn)<T> +import("./use-resource").[ResourceReturn](#resourcereturn)<T> [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-resource-dollar.ts) diff --git a/packages/docs/src/routes/demo/state/async-joke/index.tsx b/packages/docs/src/routes/demo/state/async-joke/index.tsx new file mode 100644 index 00000000000..b4900f8568e --- /dev/null +++ b/packages/docs/src/routes/demo/state/async-joke/index.tsx @@ -0,0 +1,52 @@ +import { component$, useAsync$, useSignal } from '@qwik.dev/core'; + +export default component$(() => { + const query = useSignal(''); + + // this will first run during SSR (server) + // then re-run whenever postId changes (client) + // so this code runs both on the server and the client + const jokes = useAsync$(async ({ track, abortSignal }) => { + const url = new URL( + 'https://v2.jokeapi.dev/joke/Programming?safe-mode&amount=2' + ); + const search = track(query); + if (search) { + url.searchParams.set('contains', search); + } + + // The abortSignal is automatically aborted when this function re-runs, + // canceling any pending fetch requests. + const resp = await fetch(url, { signal: abortSignal }); + const json = (await resp.json()) as { + jokes: { setup?: string; delivery?: string; joke?: never }[]; + }; + + return json.jokes; + }); + + return ( + <> + + {jokes.loading ? ( +

Loading...

+ ) : jokes.error ? ( +
Error: {jokes.error.message}
+ ) : jokes.value ? ( +
    + {jokes.value.map((joke, i) => ( +
  • +
    + {joke.joke || `${joke.setup}\n${joke.delivery}`} +
    +
  • + ))} +
+ ) : ( +

No jokes found

+ )} + + ); +}); diff --git a/packages/docs/src/routes/demo/state/resource-agify/index.tsx b/packages/docs/src/routes/demo/state/resource-agify/index.tsx deleted file mode 100644 index 61c53931478..00000000000 --- a/packages/docs/src/routes/demo/state/resource-agify/index.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { component$, useSignal, useResource$, Resource } from '@qwik.dev/core'; - -export default component$(() => { - const name = useSignal(); - - const ageResource = useResource$<{ - name: string; - age: number; - count: number; - }>(async ({ track, cleanup }) => { - track(() => name.value); - const abortController = new AbortController(); - cleanup(() => abortController.abort('cleanup')); - const res = await fetch(`https://api.agify.io?name=${name.value}`, { - signal: abortController.signal, - }); - return res.json(); - }); - - return ( -
-
- -
-

Loading...

} - onRejected={() =>

Failed to person data

} - onResolved={(ageGuess) => { - return ( -

- {name.value && ( - <> - {ageGuess.name} {ageGuess.age} years - - )} -

- ); - }} - /> -
- ); -}); diff --git a/packages/docs/src/routes/demo/state/resource-joke/index.tsx b/packages/docs/src/routes/demo/state/resource-joke/index.tsx deleted file mode 100644 index ae79533f12a..00000000000 --- a/packages/docs/src/routes/demo/state/resource-joke/index.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { component$, useResource$, Resource, useSignal } from '@qwik.dev/core'; - -export default component$(() => { - const query = useSignal('busy'); - const jokes = useResource$<{ value: string }[]>( - async ({ track, cleanup }) => { - track(() => query.value); - // A good practice is to use `AbortController` to abort the fetching of data if - // new request comes in. We create a new `AbortController` and register a `cleanup` - // function which is called when this function re-runs. - const controller = new AbortController(); - cleanup(() => controller.abort()); - - if (query.value.length < 3) { - return []; - } - - const url = new URL('https://api.chucknorris.io/jokes/search'); - url.searchParams.set('query', query.value); - - const resp = await fetch(url, { signal: controller.signal }); - const json = (await resp.json()) as { result: { value: string }[] }; - - return json.result; - } - ); - - return ( - <> - - - <>loading...} - onResolved={(jokes) => ( -
    - {jokes.map((joke, i) => ( -
  • {joke.value}
  • - ))} -
- )} - /> - - ); -}); diff --git a/packages/docs/src/routes/demo/state/resource/index.tsx b/packages/docs/src/routes/demo/state/resource/index.tsx deleted file mode 100644 index 96213474147..00000000000 --- a/packages/docs/src/routes/demo/state/resource/index.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { component$, Resource, useResource$, useSignal } from '@qwik.dev/core'; - -export default component$(() => { - const postId = useSignal('23'); - - const postTitle = useResource$(async ({ track, cleanup }) => { - // It will run first on mount (server), then re-run whenever postId changes (client) - // this means this code will run on the server and the browser - const controller = new AbortController(); - track(() => postId.value); - cleanup(() => controller.abort()); - - try { - const response = await fetch( - `https://jsonplaceholder.typicode.com/posts/${postId.value}`, - { signal: controller.signal } - ); - const data = await response.json(); - return data.title as string; - } catch (e) { - // For demo purposes only, we recommend not to use try/catch inside useResource$ - // and instead use the `onRejected` handler on the `` component - return `invalid post '${postId.value}'`; - } - }); - - return ( - <> - -

Post#{postId}:

-

Loading...

} - onResolved={(title) =>

{title}

} - /> - - ); -}); diff --git a/packages/docs/src/routes/docs/(qwik)/core/overview/index.mdx b/packages/docs/src/routes/docs/(qwik)/core/overview/index.mdx index cb359946ff7..98a13307059 100644 --- a/packages/docs/src/routes/docs/(qwik)/core/overview/index.mdx +++ b/packages/docs/src/routes/docs/(qwik)/core/overview/index.mdx @@ -545,7 +545,7 @@ Note the `string extends C`, this is only true when TypeScript cannot infer the - [`useTask$()`](../tasks/index.mdx#usetask) - defines a callback that will be called before render and/or when a watched store changes - [`useVisibleTask$()`](../tasks/index.mdx#usevisibletask) - defines a callback that will be called after rendering in the client only (browser) -- [`useResource$()`](../state/index.mdx#useresource) - creates a resource to asynchronously load data +- [`useAsync$()`](../state/index.mdx#useasync) - creates an async signal to asynchronously compute a value ### Other diff --git a/packages/docs/src/routes/docs/(qwik)/core/state/index.mdx b/packages/docs/src/routes/docs/(qwik)/core/state/index.mdx index c200a164112..8cf428f5cda 100644 --- a/packages/docs/src/routes/docs/(qwik)/core/state/index.mdx +++ b/packages/docs/src/routes/docs/(qwik)/core/state/index.mdx @@ -36,7 +36,7 @@ contributors: - Jemsco - shairez - ianlet -updated_at: '2025-03-17T12:00:00Z' +updated_at: '2026-02-10T12:00:00Z' created_at: '2023-03-20T23:45:13Z' --- @@ -224,9 +224,8 @@ 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 `useAsync$()` which is similar to `useComputed$()` but allows the computed function to be asynchronous. -2. [`useResource$()`](/docs/(qwik)/core/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). +2. `useAsync$()`: `useAsync$()` is the preferred way for asynchronous computed values. Use it 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), or performing a long computation in a WebWorker. In addition to the two ways of creating computed values described above, there is also a lower-level ([`useTask$()`](/docs/(qwik)/core/tasks/index.mdx#usetask)). This way does not produce a new signal, but rather modifies the existing state or produces a side effect. @@ -269,215 +268,178 @@ You can use this to instantiate a custom class. ### `useAsync$()` -`useAsync$()` is similar to `useComputed$()`, but it allows the computed function to be asynchronous. It returns a signal that has the result of the async function.If you read it before the async function has completed, it will stop execution and re-run the reading function when the async function is resolved. +`useAsync$()` is similar to `useComputed$()`, but it allows the compute function to be asynchronous. It returns an `AsyncSignal` that has the result of the async function. The common use case is to fetch data asynchronously, possibly based on other signals (you need to read those with `track()`). -### `useResource$()` +A common use case is to fetch data from an external API within the component, which can occur either on the server or the client. -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. +#### Example -A common use of `useResource$()` is to fetch data from an external API within the component, which can occur either on the server or the client. +This example will fetch a list of jokes based on the query typed by the user, automatically reacting to changes in the query, including aborting requests that are currently pending. -The `useResource$` hook is meant to be used with ``. The `` component is a convenient way to render different UI based on the state of the resource. - - -```tsx {11} /useResource$/ -import { - component$, - Resource, - useResource$, - useSignal, -} from '@qwik.dev/core'; + +```tsx {11} /useAsync$/ +import { component$, useAsync$, useSignal } from '@qwik.dev/core'; export default component$(() => { - const postId = useSignal('23'); + const query = useSignal(''); + + // this will first run during SSR (server) + // then re-run whenever postId changes (client) + // so this code runs both on the server and the client + const jokes = useAsync$(async ({ track, abortSignal }) => { + const url = new URL( + 'https://v2.jokeapi.dev/joke/Programming?safe-mode&amount=2' + ); + const search = track(query); + if (search) { + url.searchParams.set('contains', search); + } - const postTitle = useResource$(async ({ track }) => { - // it will run first on mount (server), then re-run whenever postId changes (client) - // this means this code will run on the server and the browser - track(() => postId.value); + // The abortSignal is automatically aborted when this function re-runs, + // canceling any pending fetch requests. + const resp = await fetch(url, { signal: abortSignal }); + const json = (await resp.json()) as { + jokes: { setup?: string; delivery?: string; joke?: never }[]; + }; - const response = await fetch( - `https://jsonplaceholder.typicode.com/posts/${postId.value}` - ); - const data = await response.json(); - return data.title as string; + return json.jokes; }); return ( <> - -

Post#{postId}:

-

Loading...

} - onResolved={(title) =>

{title}

} - /> + + {jokes.loading ? ( +

Loading...

+ ) : jokes.error ? ( +
Error: {jokes.error.message}
+ ) : jokes.value ? ( +
    + {jokes.value.map((joke, i) => ( +
  • +
    + {joke.joke || `${joke.setup}\n${joke.delivery}`} +
    +
  • + ))} +
+ ) : ( +

No jokes found

+ )} ); }); ```
+As we see in the example above, `useAsync$()` returns an `AsyncSignal` object that works like a reactive promise, containing the data and the async state. - -> **Note:** The important thing to understand about `useResource$` is that it executes on the initial component render (just like `useTask$`). Often times it is desirable to start fetching the data on the server as part of the initial HTTP request before the component is rendered. Fetching data as part of Server-Side Rendering (SSR) is a common and preferred method of data loading, typically handled by the [`routeLoader$`](/docs/(qwikrouter)/route-loader/index.mdx) API. `useResource$` is more of a low-level API that is useful when you want to fetch data in the browser. +> **Note:** The important thing to understand about `useAsync$` is that it executes on the initial component render (just like `useTask$`). +> Often times it is desirable to start fetching the data on the server as part of the initial HTTP request, even before the component is rendered. Fetching data based on the path as part of Server-Side Rendering (SSR) is a common and preferred method of data loading, typically handled by the [`routeLoader$`](/docs/(qwikrouter)/route-loader/index.mdx) API. `useAsync$` is more of a low-level API that is useful when you want to fetch data in the browser. > +> In many ways `useAsync$` is similar to `useTask$`. The big differences are: > -> In many ways `useResource$` is similar to `useTask$`. The big differences are: -> -> - `useResource$` allows you to return a "value". -> - `useResource$` does not block rendering while the resource is being resolved. +> - `useAsync$` allows you to return a "value" and exposes it via the `AsyncSignal`. +> - `useAsync$` does not block rendering while the value is being resolved. +> - `useAsync$` provides reactive `.loading` and `.error` properties, which can be used to show loading spinners or error messages in the UI. +> `useAsync$` also provides an `abortSignal` that can be used to cancel the async operation when the component is destroyed or when the async function is re-run due to a change in tracked state. +> `useAsync$` has built-in support for polling with `interval`, which allows you to re-run the async function at a specified interval, as long as it is used somewhere. +> `useTask$` on the other hand is useful for running any code that doesn't need to return a value or directly affect the rendering of the component, implementing logic based on reactive state. > > See [`routeLoader$`](/docs/(qwikrouter)/route-loader/index.mdx) for fetching data early as part of initial HTTP request. -> **NOTE**: During SSR the `` component will pause rendering until the resource is resolved. This way the SSR will not render with the loading indicator. - -#### Advanced example +#### Channel example -A more complete example of fetching data with `AbortController`, `track` and `cleanup`. This example will fetch a list of jokes based on the query typed by the user, -automatically reacting to changes in the query, including aborting requests that are currently pending. +In this example, we use `useAsync$()` to get updates from a server using Server-Sent Events (SSE). Note that it writes to its own value, updating its subscribers. +To be able to refer to itself, the signal needs to be in an object. - -```tsx {11} /useResource$/ -import { - component$, - useResource$, - Resource, - useSignal, -} from '@qwik.dev/core'; +```tsx +import { component$, useAsync$ } from '@qwik.dev/core'; export default component$(() => { - const query = useSignal('busy'); - const jokes = useResource$<{ value: string }[]>( - async ({ track, cleanup }) => { - track(() => query.value); - // A good practice is to use `AbortController` to abort the fetching of data if - // new request comes in. We create a new `AbortController` and register a `cleanup` - // function which is called when this function re-runs. - const controller = new AbortController(); - cleanup(() => controller.abort()); - - if (query.value.length < 3) { - return []; - } + const _ref = {} as { state: AsyncSignal }; + _ref.state = useAsync$( + ({ track, abortSignal }) => { + const url = new URL('/api/state-channel'); + const eventSource = new EventSource(url); - const url = new URL('https://api.chucknorris.io/jokes/search'); - url.searchParams.set('query', query.value); + eventSource.onmessage = (event) => { + _ref.state.value = event.data; + }; - const resp = await fetch(url, { signal: controller.signal }); - const json = (await resp.json()) as { result: { value: string }[] }; + abortSignal.addEventListener('abort', () => { + eventSource.close(); + }); - return json.result; - } + return 'init'; + }, + // Close the channel as soon as there are no subscribers + { eagerCleanup: true } ); + const state = _ref.state; - return ( - <> - - - <>loading...} - onResolved={(jokes) => ( -
    - {jokes.map((joke, i) => ( -
  • {joke.value}
  • - ))} -
- )} - /> - - ); + return
Current state is: {state.value}
; }); ``` -
- -As we see in the example above, `useResource$()` returns a `ResourceReturn` object that works like a reactive promise, containing the data and the resource state. -The state `resource.loading` can be one of the following: +#### API -- `false` - the data is not yet available. -- `true` - the data is available. (Either resolved or rejected.) +The API of `useAsync$()` (and `createAsync$()`) is as follows: +```tsx +function useAsync$( + asyncFn: (opts: { + track: (signal: Signal) => U; + cleanup: (fn: () => void) => void; + abortSignal: AbortSignal; + previous: T | undefined; + }) => Promise, + options?: { initial?: T; interval?: number; concurrency?: number; timeout?: number; eagerCleanup?: boolean } +): AsyncSignal; +``` -The callback passed to [`useResource$()`](/docs/(qwik)/core/state/index.mdx#useresource) runs right after the [`useTask$()`](/docs/(qwik)/core/tasks/index.mdx#usetask) callbacks complete. Please refer to the [Lifecycle](../tasks/index.mdx#lifecycle) section for more details. +Arguments: -#### `` +- The `track` and `cleanup` functions are the same as in `useTask$`, used to track reactive signals and clean up resources when the async function is re-run or the component is destroyed. -`` is a component meant to be used with the `useResource$()` that renders different content depending on if the resource is pending, resolved, or rejected. +- The `abortSignal` is automatically aborted when the async function is re-run or the component is destroyed, which allows you to cancel any pending async operations. Best practice is to pass it to every API that supports it. Also, read from it after every await, and if it is aborted, stop the execution of the async function. (`if (abortSignal.aborted) return;`) -```tsx -
Loading...
} - onRejected={() =>
Failed to load weather
} - onResolved={(weather) => { - return
Temperature: {weather.temp}
; - }} -/> -``` +- `previous` is the previous resolved value of the async function, which can be useful to implement logic based on the previous value. -It is worth noting that `` is not required when using `useResource$()`. It is just a convenient way to render the resource state. +The callback passed to `useAsync$()` runs as soon as it is read. That means that if you use it in the JSX output, it will start during the initial render of that part of the JSX. So if you pass it to a component, it will start when that component is rendered, after tasks have run. If you want to start it immediately on initialization, you can call `asyncSignal.promise()` to start it immediately. This can also be used to await the completion, but you still have to read the value or error from the signal itself. -This example shows how `useResource$` is used to perform a fetch call to the [agify.io](https://agify.io/) API. This will guess a person's age based on the name typed by the user, and will update whenever the user types in the name input. +Options: - -```tsx {11} /useResource$/ -import { - component$, - useSignal, - useResource$, - Resource, -} from '@qwik.dev/core'; +- `initial`: the initial value of the async signal before the async function resolves. This is useful to show some initial data while the async function is still loading. If not provided, reading the value before it's resolved will throw a `Promise`, which is Qwik's way of ensuring the current function will re-run when the value is available. +- `interval`: if provided, the async function will be re-run every `interval` milliseconds, as long as the async signal is used somewhere. This is useful to keep data fresh. +- `concurrency`: controls what happens when the async function is still running and it needs to be re-run (because of a change in tracked signals or because of polling). + - The default is `concurrency: 1`, which means that the currently running async function and its cleanups have to complete before a new one starts. + - If set to a number, there can be be up to that number currently in flight before new calls need to wait for one of them to complete. `0` means no limit. + - No matter the `concurrency` setting, whenever the signal is invalidated, the currently running async function will be aborted using the `abortSignal`. It's up to you to decide to use the signal or not. + - The cleanups are awaited, so if you do not want that, you should not return a Promise. +- `timeout`: if provided, the async function will be automatically aborted if it takes longer than the specified time to resolve. +- `eagerCleanup`: if set to `true`, the async function will be aborted as soon as there are no subscribers to the async signal, instead of waiting for the next time it needs to be re-run. This is useful to free up resources as soon as they are not needed anymore. -export default component$(() => { - const name = useSignal(); - - const ageResource = useResource$<{ - name: string; - age: number; - count: number; - }>(async ({ track, cleanup }) => { - track(() => name.value); - const abortController = new AbortController(); - cleanup(() => abortController.abort('cleanup')); - const res = await fetch(`https://api.agify.io?name=${name.value}`, { - signal: abortController.signal, - }); - return res.json(); - }); +The returned `AsyncSignal` has the following (reactive) properties: - return ( -
-
- -
-

Loading...

} - onRejected={() =>

Failed to person data

} - onResolved={(ageGuess) => { - return ( -

- {name.value && ( - <> - {ageGuess.name} {ageGuess.age} years - - )} -

- ); - }} - /> -
- ); -}); +```ts +interface AsyncSignal { + value: T; + loading: boolean; + error: Error | undefined; + interval: number; + promise(): Promise; + abort(reason?: any): void; +} ``` -
+ +- `value: T`: the resolved value (or initial value if not yet resolved). Note that reading this property before the async function has completed will throw a `Promise` for the result, which causes Qwik to wait until that resolves and then re-run the reading function. Writing a different value to this property will update the value and notify subscribers. +- `loading: boolean`: whether the async function is currently running +- `error: Error | undefined`: the error if the async function failed +- `interval: number`: the current polling interval in milliseconds. This can be updated to change the polling interval or to stop polling by setting it to `0`. +- `promise(): Promise`: a function that can be called to start the async function. It returns a promise that resolves when the async function completes, but not its result. That needs to be read from `value` or `error`. +- `abort(reason?: any): void`: a function that can be called to abort the currently running async function and run cleanups if needed. The reason is passed to the abortSignal. ## Passing state diff --git a/packages/docs/src/routes/docs/(qwik)/core/tasks/index.mdx b/packages/docs/src/routes/docs/(qwik)/core/tasks/index.mdx index 1d81127a217..a235c80a50d 100644 --- a/packages/docs/src/routes/docs/(qwik)/core/tasks/index.mdx +++ b/packages/docs/src/routes/docs/(qwik)/core/tasks/index.mdx @@ -20,7 +20,8 @@ contributors: - aendel - jemsco - varixo -updated_at: '2025-11-08T03:33:22Z' + - wmertens +updated_at: '2026-02-10T12:00:00Z' created_at: '2023-03-31T02:40:50Z' --- @@ -38,7 +39,7 @@ Tasks are meant for running asynchronous operations as part of component initial > - Tasks run before rendering and can block rendering. > - Subsequent task re-executions (when tracked state changes) block rendering by default, but can be configured to not block DOM updates with `deferUpdates: false`. -`useTask$()` should be your default go-to API for running either synchronous or asynchronous work as part of component initialization or state change. It is only when you can't achieve what you need with `useTask$()` that you should consider using `useVisibleTask$()` or `useResource$()`. +`useTask$()` should be your default go-to API for running either synchronous or asynchronous work as part of component initialization or state change. It is only when you can't achieve what you need with `useTask$()` that you should consider using `useVisibleTask$()`. The basic use case for `useTask$()` is to perform work on component initialization. `useTask$()` has these properties: - It can run on either the server or in the browser. @@ -50,7 +51,7 @@ Tasks can also be used to perform work when a component state changes. In this c Sometimes a task needs to run only on the browser and after rendering, in that case, you should use [`useVisibleTask$()`](#usevisibletask). -> **Note**: If you need to fetch data asynchronously and not block rendering, you should use [`useResource$()`](/docs/core/state/#useresource). [`useResource$()`](/docs/core/state/#useresource) does not block rendering while the resource is being resolved. +> **Note**: If you need to fetch data asynchronously and not block rendering, you should use [`useAsync$()`](/docs/core/state/#useasync). `useAsync$()` does not block rendering while the async value is being resolved. ## Lifecycle Resumability is "Lazy execution", it's the ability to build the "framework state" (component boundaries, etc) on the server, and have it exist on the client without re-executing the framework again. @@ -179,7 +180,7 @@ Use `useTask$()` when you need to: - Run code only once before the component is first rendered - Programmatically run side-effect code when state changes -> Note, if you're thinking about loading data using `fetch()` inside of `useTask$`, consider using [`useResource$()`](/docs/core/state/#useresource) instead. This API is more efficient in terms of leveraging SSR streaming and parallel data fetching. +> Note, if you're thinking about loading data using `fetch()` inside of `useTask$`, consider using [`useAsync$()`](/docs/core/state/#useasync) instead. This API is more convenient. ### On mount @@ -215,7 +216,7 @@ Note that deep store updates don't mutate the store itself, so `store[item].coun During SSR, each component will wait until all tasks are completed before outputting the HTML for that component. So a task can be called multiple times during SSR if the tracked state changes due to other tasks. -> **Note**: If all you want to do is compute a new state from an existing state synchronously, you should use [`useComputed$()`](/docs/core/state/#usecomputed) instead. +> **Note**: If all you want to do is compute a new state from an existing state synchronously, use [`useComputed$()`](/docs/core/state/#usecomputed). For async derivations (like search-as-you-type), use [`useAsync$()`](/docs/core/state/#useasync) and cancel in-flight work with `cleanup()` and `AbortController`. ```tsx diff --git a/packages/docs/src/routes/docs/(qwikrouter)/guides/best-practices/index.mdx b/packages/docs/src/routes/docs/(qwikrouter)/guides/best-practices/index.mdx index c580c9191d7..e05868200b5 100644 --- a/packages/docs/src/routes/docs/(qwikrouter)/guides/best-practices/index.mdx +++ b/packages/docs/src/routes/docs/(qwikrouter)/guides/best-practices/index.mdx @@ -64,7 +64,7 @@ export default component$(() => { ``` -## Moving signal reads to `useTask$` or `useComputed$` +## Moving signal reads to `useTask$`, `useComputed$`, or `useAsync$` This is similar to the tip above. @@ -73,7 +73,7 @@ the function that the "read" is happening at, will re-run again on every change to that signal. That’s why it’s better to "read" values (and track them) -inside of `useTask$` or `useComputed$` functions instead of inside component functions, +inside of `useTask$`, `useComputed$`, or `useAsync$` functions instead of inside component functions, whenever possible. Because otherwise, your component function will re-run @@ -105,6 +105,23 @@ export default component$(() => { }); ``` +For async derived values (like search suggestions), use `useAsync$` and abort in-flight work on each keystroke: + +```tsx title="Async derived value with cancellation" +export default component$(() => { + const query = useSignal(''); + const results = useAsync$(async ({ track, abortSignal }) => { + const q = track(query); + const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`, { + signal: abortSignal, + }); + return res.json(); + }, { initial: [] }); + + return
{results.value.length} results
; +}); +``` + ## Use `useVisibleTask$()` as a last resort Although convenient, `useVisibleTask$()` runs code eagerly and blocks the main thread, preventing user interaction until the task is finished. You can think of it as an escape hatch. diff --git a/packages/docs/src/routes/examples/apps/partial/hackernews-index/app.tsx b/packages/docs/src/routes/examples/apps/partial/hackernews-index/app.tsx index b50097e3db5..b903e3e3a6e 100644 --- a/packages/docs/src/routes/examples/apps/partial/hackernews-index/app.tsx +++ b/packages/docs/src/routes/examples/apps/partial/hackernews-index/app.tsx @@ -1,26 +1,31 @@ -import { component$, isServer, useSignal, useStyles$, useTask$ } from '@qwik.dev/core'; +import { component$, useAsync$, useSignal, useStyles$, type Signal } from '@qwik.dev/core'; import HackerNewsCSS from './hacker-news.css?inline'; export const HackerNews = component$(() => { useStyles$(HackerNewsCSS); - const data = useSignal(); + const page = useSignal(0); - useTask$(async () => { - if (isServer) { - const response = await fetch('https://node-hnapi.herokuapp.com/news?page=0'); - data.value = await response.json(); - } + const data = useAsync$(async ({ track, abortSignal }) => { + const pageNum = track(page); + const response = await fetch(`https://node-hnapi.herokuapp.com/news?page=${pageNum}`, { + signal: abortSignal, + }); + return await response.json(); }); return (
); }); -export const Nav = component$(() => { +const Loading = component$(() => { + return
Loading...
; +}); + +const Nav = component$(() => { return (