\n\n_(Optional)_ Block the rendering of the component until the task completes. Default is `true`\n\n\n
\n
",
+ "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-task.ts",
+ "mdFile": "core.taskoptions.md"
+ },
{
"name": "Tracker",
"id": "tracker",
@@ -2626,7 +2640,7 @@
}
],
"kind": "Function",
- "content": "Reruns the `taskFn` when the observed inputs change.\n\nUse `useTask` to observe changes on a set of inputs, and then re-execute the `taskFn` when those inputs change.\n\nThe `taskFn` only executes if the observed inputs change. To observe the inputs, use the `obs` function to wrap property reads. This creates subscriptions that will trigger the `taskFn` to rerun.\n\n\n```typescript\nuseTask$: (fn: TaskFn) => void\n```\n\n\n
\n\nParameter\n\n\n
\n\nType\n\n\n
\n\nDescription\n\n\n
\n
\n\nfn\n\n\n
\n\n[TaskFn](#taskfn)\n\n\n
\n\n\n
\n
\n\n**Returns:**\n\nvoid",
+ "content": "Reruns the `taskFn` when the observed inputs change.\n\nUse `useTask` to observe changes on a set of inputs, and then re-execute the `taskFn` when those inputs change.\n\nThe `taskFn` only executes if the observed inputs change. To observe the inputs, use the `obs` function to wrap property reads. This creates subscriptions that will trigger the `taskFn` to rerun.\n\n\n```typescript\nuseTask$: (fn: TaskFn, opts?: TaskOptions) => void\n```\n\n\n
+
+_(Optional)_ Block the rendering of the component until the task completes. Default is `true`
+
+
+
+
+[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-task.ts)
+
## Tracker
Used to signal to Qwik which state should be watched for changes.
@@ -9955,7 +9997,7 @@ Use `useTask` to observe changes on a set of inputs, and then re-execute the `ta
The `taskFn` only executes if the observed inputs change. To observe the inputs, use the `obs` function to wrap property reads. This creates subscriptions that will trigger the `taskFn` to rerun.
```typescript
-useTask$: (fn: TaskFn) => void
+useTask$: (fn: TaskFn, opts?: TaskOptions) => void
```
@@ -9981,6 +10023,19 @@ fn
+
+
+
+opts
+
+
+
+[TaskOptions](#taskoptions)
+
+
+
+_(Optional)_
+
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 0981c062752..1d81127a217 100644
--- a/packages/docs/src/routes/docs/(qwik)/core/tasks/index.mdx
+++ b/packages/docs/src/routes/docs/(qwik)/core/tasks/index.mdx
@@ -19,7 +19,8 @@ contributors:
- adamdbradley
- aendel
- jemsco
-updated_at: '2023-10-18T07:33:22Z'
+ - varixo
+updated_at: '2025-11-08T03:33:22Z'
created_at: '2023-03-31T02:40:50Z'
---
@@ -33,15 +34,17 @@ Tasks are meant for running asynchronous operations as part of component initial
> **Note**: Tasks are similar to `useEffect()` in React, but there are enough differences that we did not want to call them the same so as not to bring preexisting expectations about how they work. The main differences are:
>
> - Tasks are asynchronous.
-> - Task run on server and browser.
+> - Tasks run on server and browser.
> - 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$()`.
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.
-- It runs before rendering and blocks rendering.
+- It runs before the initial rendering and blocks the initial render.
- If multiple tasks are running then they are run sequentially in the order they were registered. An asynchronous task will block the next task from running until it completes.
+- By default, subsequent re-executions (when tracked state changes) do block DOM updates unless `deferUpdates: false` is specified.
Tasks can also be used to perform work when a component state changes. In this case, the task will rerun every time the tracked state changes. See: [`track()`](#track).
@@ -59,7 +62,7 @@ When the user interacts with the application, it resumes on the client-side, con
**In Qwik, there are only 3 lifecycle stages:**
-- `Task` - run before rendering and when tracked state changes. `Tasks` run sequentially, and block rendering.
+- `Task` - run before rendering and when tracked state changes. `Tasks` run sequentially, and block each other. They also can block rendering.
- `Render` - runs after `TASK` and before `VisibleTask`
- `VisibleTask` - runs after `Render` and when the component becomes visible
@@ -93,6 +96,28 @@ When the user interacts with the application, it resumes on the client-side, con
`useTask$()` registers a hook to be executed upon component creation, it will run at least once either in the server or in the browser, depending on where the component is initially rendered.
+### Task options
+
+`useTask$()` accepts an optional second parameter of type `TaskOptions` to configure the task behavior.
+
+```typescript
+interface TaskOptions {
+ deferUpdates?: boolean;
+}
+```
+
+#### `deferUpdates`
+
+The `deferUpdates` option controls whether subsequent task re-executions (when tracked state changes) should block DOM updates.
+
+**Default behavior (`deferUpdates: false` or not specified):**
+- **Initial render**: The task blocks rendering (same as default)
+- **Subsequent runs**: The task blocks DOM updates until it completes - the journal flush is deferred until the task finishes
+
+**With `deferUpdates: true`:**
+- **Initial render**: The task blocks rendering (runs before the component renders for the first time)
+- **Subsequent runs**: The task runs asynchronously without blocking DOM updates
+
Additionally, this task can be reactive and will re-execute when **tracked** [state](/docs/(qwik)/core/state/index.mdx) changes.
**Notice that any subsequent re-execution of the task will always happen in the browser**, because reactivity is a browser-only thing.
@@ -110,7 +135,7 @@ Additionally, this task can be reactive and will re-execute when **tracked** [st
> If `useTask$()` does not track any state, it will run **exactly once**, either in the server **or** in the browser (**not both**), depending where the component is initially rendered. Effectively behaving like an "on-mount" hook.
-`useTask$()` will block the rendering of the component until after its async callback resolves, in other words, tasks execute sequentially even if they are asynchronous. (Only one task executes at a time).
+`useTask$()` will block the rendering of the component until after its async callback resolves. Tasks execute sequentially even if they are asynchronous (only one task executes at a time within a component). Subsequent re-executions (when tracking state changes) run asynchronously by default and block rendering unless `deferUpdates: false` is set.
Take a look at the simplest use case of the task to run some asynchronous work on component initialization:
@@ -151,7 +176,7 @@ const delay = (time: number) => new Promise((res) => setTimeout(res, time));
Use `useTask$()` when you need to:
- Run async tasks before rendering
-- Run code only once before the component is first rendered
+- 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.
diff --git a/packages/docs/vite.config.ts b/packages/docs/vite.config.ts
index 578244e9fdf..0ae8ad5dfa2 100644
--- a/packages/docs/vite.config.ts
+++ b/packages/docs/vite.config.ts
@@ -144,6 +144,7 @@ export default defineConfig(() => {
'qwik-image',
// optimizing breaks the wasm import
'@rolldown/browser',
+ '@qwik.dev/devtools',
],
},
preview: {
diff --git a/packages/qwik/src/core/index.ts b/packages/qwik/src/core/index.ts
index 240b3aadb65..86b3d904824 100644
--- a/packages/qwik/src/core/index.ts
+++ b/packages/qwik/src/core/index.ts
@@ -143,7 +143,7 @@ export { useComputedQrl } from './use/use-computed';
export { useSerializerQrl, useSerializer$ } from './use/use-serializer';
export type { OnVisibleTaskOptions, VisibleTaskStrategy } from './use/use-visible-task';
export { useVisibleTaskQrl } from './use/use-visible-task';
-export type { TaskCtx, TaskFn, Tracker } from './use/use-task';
+export type { TaskCtx, TaskFn, Tracker, TaskOptions } from './use/use-task';
export type {
ResourceProps,
ResourceOptions,
diff --git a/packages/qwik/src/core/qwik.core.api.md b/packages/qwik/src/core/qwik.core.api.md
index e4667021665..536b668d225 100644
--- a/packages/qwik/src/core/qwik.core.api.md
+++ b/packages/qwik/src/core/qwik.core.api.md
@@ -1662,6 +1662,11 @@ export interface TaskCtx {
// @public (undocumented)
export type TaskFn = (ctx: TaskCtx) => ValueOrPromise void)>;
+// @public (undocumented)
+export interface TaskOptions {
+ deferUpdates?: boolean;
+}
+
// @internal (undocumented)
export class _TextVNode extends _VNode {
constructor(flags: _VNodeFlags, parent: _ElementVNode | _VirtualVNode | null, previousSibling: _VNode | null | undefined, nextSibling: _VNode | null | undefined, textNode: Text | null, text: string | undefined);
@@ -1803,12 +1808,12 @@ export interface UseStylesScoped {
export const useStylesScopedQrl: (styles: QRL) => UseStylesScoped;
// @public
-export const useTask$: (fn: TaskFn) => void;
+export const useTask$: (fn: TaskFn, opts?: TaskOptions) => void;
// Warning: (ae-internal-missing-underscore) The name "useTaskQrl" should be prefixed with an underscore because the declaration is marked as @internal
//
// @internal (undocumented)
-export const useTaskQrl: (qrl: QRL) => void;
+export const useTaskQrl: (qrl: QRL, opts?: TaskOptions) => void;
// @public
export const useVisibleTask$: (fn: TaskFn, opts?: OnVisibleTaskOptions) => void;
diff --git a/packages/qwik/src/core/shared/scheduler.ts b/packages/qwik/src/core/shared/scheduler.ts
index 13d580a08d1..266f0cba866 100644
--- a/packages/qwik/src/core/shared/scheduler.ts
+++ b/packages/qwik/src/core/shared/scheduler.ts
@@ -737,11 +737,16 @@ This is often caused by modifying a signal in an already rendered component duri
host
) as ValueOrPromise>;
} else {
- returnValue = runTask(
- payload as Task,
- container,
- host
- ) as ValueOrPromise>;
+ const task = payload as Task;
+ returnValue = runTask(task, container, host) as ValueOrPromise<
+ ChoreReturnValue
+ >;
+ if (task.$flags$ & TaskFlags.RENDER_BLOCKING) {
+ blockingChoresCount++;
+ returnValue = maybeThen(returnValue, () => {
+ blockingChoresCount--;
+ });
+ }
}
}
break;
diff --git a/packages/qwik/src/core/tests/use-task.spec.tsx b/packages/qwik/src/core/tests/use-task.spec.tsx
index 24033f45d2e..b69a036bdac 100644
--- a/packages/qwik/src/core/tests/use-task.spec.tsx
+++ b/packages/qwik/src/core/tests/use-task.spec.tsx
@@ -12,7 +12,7 @@ import {
type Signal as SignalType,
} from '@qwik.dev/core';
import { domRender, getTestPlatform, ssrRenderToDom, trigger } from '@qwik.dev/core/testing';
-import { describe, expect, it } from 'vitest';
+import { describe, expect, it, vi } from 'vitest';
import { ErrorProvider } from '../../testing/rendering.unit-util';
import { delay } from '../shared/utils/promises';
import { WrappedSignalImpl } from '../reactive-primitives/impl/wrapped-signal-impl';
@@ -653,6 +653,164 @@ describe.each([
);
});
+ describe('blockRender', () => {
+ it('should execute task and block render until finish', async () => {
+ vi.useFakeTimers();
+ (global as any).counter = 0;
+ const Counter = component$(() => {
+ const count = useSignal(0);
+ const text = useSignal('val1');
+
+ useTask$(
+ async ({ track }) => {
+ const c = track(count);
+ // skip initial render
+ if ((global as any).counter > 0) {
+ text.value = 'val' + (c + 1);
+ await delay(100);
+ }
+ (global as any).counter++;
+ },
+ {
+ deferUpdates: true,
+ }
+ );
+ return (
+
+ );
+ });
+
+ // Start rendering
+ const renderPromise = render(, { debug });
+ // Advance timers to complete rendering
+ await vi.advanceTimersToNextTimerAsync();
+ const { document } = await renderPromise;
+
+ // Initial render
+ await expect(document.body.firstChild).toMatchDOM();
+
+ // FIRST CLICK
+
+ // Trigger task by clicking
+ let triggerPromise = trigger(document.body, 'button', 'click');
+
+ // Advance timers but not enough to complete the delay
+ await vi.advanceTimersByTimeAsync(99);
+ // Should be still old value
+ await expect(document.body.firstChild).toMatchDOM();
+ // Advance timers to complete the delay
+ await vi.advanceTimersByTimeAsync(1);
+ // Wait for the trigger to complete
+ await triggerPromise;
+
+ // Should have the new value
+ await expect(document.body.firstChild).toMatchDOM();
+
+ // SECOND CLICK
+
+ // Trigger task by clicking
+ triggerPromise = trigger(document.body, 'button', 'click');
+
+ // Advance timers but not enough to complete the delay
+ await vi.advanceTimersByTimeAsync(99);
+ // Should be still old value
+ await expect(document.body.firstChild).toMatchDOM();
+ // Advance timers to complete the delay
+ await vi.advanceTimersByTimeAsync(1);
+ // Wait for the trigger to complete
+ await triggerPromise;
+
+ // Should have the new value
+ await expect(document.body.firstChild).toMatchDOM();
+
+ vi.useRealTimers();
+ });
+
+ it('should execute task and not block render until finish', async () => {
+ vi.useFakeTimers();
+ (global as any).counter = 0;
+ const Counter = component$(() => {
+ const count = useSignal(0);
+ const text = useSignal('val1');
+
+ useTask$(
+ async ({ track }) => {
+ const c = track(count);
+ // skip initial render
+ if ((global as any).counter > 0) {
+ text.value = 'val' + (c + 1);
+ await delay(100);
+ }
+ (global as any).counter++;
+ },
+ {
+ deferUpdates: false,
+ }
+ );
+ return (
+
+ );
+ });
+
+ // Start rendering
+ const renderPromise = render(, { debug });
+ // Advance timers to complete rendering
+ await vi.advanceTimersToNextTimerAsync();
+ const { document } = await renderPromise;
+
+ // Initial render
+ await expect(document.body.firstChild).toMatchDOM();
+
+ // FIRST CLICK
+
+ // Trigger task by clicking
+ let triggerPromise = trigger(document.body, 'button', 'click');
+
+ // Advance timers but not enough to complete the delay
+ await vi.advanceTimersByTimeAsync(99);
+ // Should have the new value
+ await expect(document.body.firstChild).toMatchDOM();
+ // Advance timers to complete the delay
+ await vi.advanceTimersByTimeAsync(1);
+ // Wait for the trigger to complete
+ await triggerPromise;
+
+ // Should have the new value
+ await expect(document.body.firstChild).toMatchDOM();
+
+ // SECOND CLICK
+
+ // Trigger task by clicking
+ triggerPromise = trigger(document.body, 'button', 'click');
+
+ // Advance timers but not enough to complete the delay
+ await vi.advanceTimersByTimeAsync(99);
+ // Should have the new value
+ await expect(document.body.firstChild).toMatchDOM();
+ // Advance timers to complete the delay
+ await vi.advanceTimersByTimeAsync(1);
+ // Wait for the trigger to complete
+ await triggerPromise;
+
+ // Should have the new value
+ await expect(document.body.firstChild).toMatchDOM();
+
+ vi.useRealTimers();
+ });
+ });
+
describe('regression', () => {
it('#5782', async () => {
const Child = component$(({ sig }: { sig: SignalType> }) => {
@@ -897,7 +1055,7 @@ describe.each([
);
});
- it('catch the ', async () => {
+ it('should catch an server side error', async () => {
const error = new Error('HANDLE ME');
const Cmp = component$(() => {
useTask$(() => {
diff --git a/packages/qwik/src/core/use/use-task-dollar.ts b/packages/qwik/src/core/use/use-task-dollar.ts
index e26ad32c3bb..b9ba8b0fd42 100644
--- a/packages/qwik/src/core/use/use-task-dollar.ts
+++ b/packages/qwik/src/core/use/use-task-dollar.ts
@@ -1,5 +1,5 @@
import { implicit$FirstArg } from '../shared/qrl/implicit_dollar';
-import { useTaskQrl, type TaskFn } from './use-task';
+import { useTaskQrl, type TaskFn, type TaskOptions } from './use-task';
//
// !!DO NOT EDIT THIS COMMENT DIRECTLY!!!
@@ -63,4 +63,7 @@ import { useTaskQrl, type TaskFn } from './use-task';
*/
//
// We need to cast to help out the api extractor
-export const useTask$ = /*#__PURE__*/ implicit$FirstArg(useTaskQrl) as (fn: TaskFn) => void;
+export const useTask$ = /*#__PURE__*/ implicit$FirstArg(useTaskQrl) as (
+ fn: TaskFn,
+ opts?: TaskOptions
+) => void;
diff --git a/packages/qwik/src/core/use/use-task.ts b/packages/qwik/src/core/use/use-task.ts
index 076b8487dfb..70868a2f497 100644
--- a/packages/qwik/src/core/use/use-task.ts
+++ b/packages/qwik/src/core/use/use-task.ts
@@ -22,6 +22,7 @@ export const enum TaskFlags {
TASK = 1 << 1,
RESOURCE = 1 << 2,
DIRTY = 1 << 3,
+ RENDER_BLOCKING = 1 << 4,
}
//
@@ -124,7 +125,6 @@ export interface TaskCtx {
/** @public */
export type TaskFn = (ctx: TaskCtx) => ValueOrPromise void)>;
-/** @public */
export interface DescriptorBase extends BackRef {
$flags$: number;
$index$: number;
@@ -134,8 +134,14 @@ export interface DescriptorBase extends BackRef {
$destroy$: NoSerialize<() => void> | null;
}
+/** @public */
+export interface TaskOptions {
+ /** Block the rendering of the component until the task completes. Default is `true` */
+ deferUpdates?: boolean;
+}
+
/** @internal */
-export const useTaskQrl = (qrl: QRL): void => {
+export const useTaskQrl = (qrl: QRL, opts?: TaskOptions): void => {
const { val, set, iCtx, i } = useSequentialScope<1 | Task>();
if (val) {
return;
@@ -143,8 +149,12 @@ export const useTaskQrl = (qrl: QRL): void => {
assertQrl(qrl);
set(1);
+ const taskFlags =
+ // enabled by default
+ opts?.deferUpdates === false ? 0 : TaskFlags.RENDER_BLOCKING;
+
const task = new Task(
- TaskFlags.DIRTY | TaskFlags.TASK,
+ TaskFlags.DIRTY | TaskFlags.TASK | taskFlags,
i,
iCtx.$hostElement$,
qrl,