Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/green-days-give.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@qwik.dev/core': minor
---

feat: introduce blockRender option for useTask$
16 changes: 15 additions & 1 deletion packages/docs/src/routes/api/qwik/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -2252,6 +2252,20 @@
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-task.ts",
"mdFile": "core.taskfn.md"
},
{
"name": "TaskOptions",
"id": "taskoptions",
"hierarchy": [
{
"name": "TaskOptions",
"id": "taskoptions"
}
],
"kind": "Interface",
"content": "```typescript\nexport interface TaskOptions \n```\n\n\n<table><thead><tr><th>\n\nProperty\n\n\n</th><th>\n\nModifiers\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\n[blockRender?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nboolean\n\n\n</td><td>\n\n_(Optional)_ If true, the task will block the rendering of the component until it is complete.\n\n\n</td></tr>\n</tbody></table>",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-task.ts",
"mdFile": "core.taskoptions.md"
},
{
"name": "Tracker",
"id": "tracker",
Expand Down Expand Up @@ -2584,7 +2598,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<table><thead><tr><th>\n\nParameter\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\nfn\n\n\n</td><td>\n\n[TaskFn](#taskfn)\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>\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<table><thead><tr><th>\n\nParameter\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\nfn\n\n\n</td><td>\n\n[TaskFn](#taskfn)\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\nopts\n\n\n</td><td>\n\n[TaskOptions](#taskoptions)\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n</tbody></table>\n\n**Returns:**\n\nvoid",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-task-dollar.ts",
"mdFile": "core.usetask_.md"
},
Expand Down
57 changes: 56 additions & 1 deletion packages/docs/src/routes/api/qwik/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8784,6 +8784,48 @@ export type TaskFn = (ctx: TaskCtx) => ValueOrPromise<void | (() => void)>;

[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-task.ts)

## TaskOptions

```typescript
export interface TaskOptions
```

<table><thead><tr><th>

Property

</th><th>

Modifiers

</th><th>

Type

</th><th>

Description

</th></tr></thead>
<tbody><tr><td>

[blockRender?](#)

</td><td>

</td><td>

boolean

</td><td>

_(Optional)_ If true, the task will block the rendering of the component until it is complete.

</td></tr>
</tbody></table>

[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.
Expand Down Expand Up @@ -9914,7 +9956,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
```

<table><thead><tr><th>
Expand All @@ -9940,6 +9982,19 @@ fn

</td><td>

</td></tr>
<tr><td>

opts

</td><td>

[TaskOptions](#taskoptions)

</td><td>

_(Optional)_

</td></tr>
</tbody></table>

Expand Down
32 changes: 30 additions & 2 deletions packages/docs/src/routes/docs/(qwik)/core/tasks/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ contributors:
- adamdbradley
- aendel
- jemsco
updated_at: '2023-10-18T07:33:22Z'
- varixo
updated_at: '2025-10-31T03:33:22Z'
created_at: '2023-03-31T02:40:50Z'
---

Expand Down Expand Up @@ -59,7 +60,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

Expand Down Expand Up @@ -93,6 +94,33 @@ 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 {
blockRender?: boolean;
}
```

#### `blockRender`

When set to `true`, the task will block the rendering of the component until the task completes. This is useful when you need to ensure that certain asynchronous operations finish before the component is rendered to the user.

**Behavior:**
- **`true`**: The task becomes render-blocking. The component will not render until the task finishes executing.
- **`false` (default)**: The task runs asynchronously without blocking the component rendering.

**Use Cases:**

Use `blockRender: true` when:
- You need to perform exit animations before removing elements from the DOM
- You want to prevent a flash of incorrect content

**Performance Considerations:**
- Use carefully as it can impact rendering performance

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.
Expand Down
1 change: 1 addition & 0 deletions packages/docs/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ export default defineConfig(() => {
'qwik-image',
// optimizing breaks the wasm import
'@rolldown/browser',
'@qwik.dev/devtools',
],
},
preview: {
Expand Down
2 changes: 1 addition & 1 deletion packages/qwik/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
9 changes: 7 additions & 2 deletions packages/qwik/src/core/qwik.core.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1665,6 +1665,11 @@ export interface TaskCtx {
// @public (undocumented)
export type TaskFn = (ctx: TaskCtx) => ValueOrPromise<void | (() => void)>;

// @public (undocumented)
export interface TaskOptions {
blockRender?: 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);
Expand Down Expand Up @@ -1806,12 +1811,12 @@ export interface UseStylesScoped {
export const useStylesScopedQrl: (styles: QRL<string>) => 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<TaskFn>) => void;
export const useTaskQrl: (qrl: QRL<TaskFn>, opts?: TaskOptions) => void;

// @public
export const useVisibleTask$: (fn: TaskFn, opts?: OnVisibleTaskOptions) => void;
Expand Down
20 changes: 15 additions & 5 deletions packages/qwik/src/core/shared/scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ export const createScheduler = (
let drainChore: Chore<ChoreType.WAIT_FOR_QUEUE> | null = null;
let drainScheduled = false;
let isDraining = false;
let blockJournalFlush = false;
let isJournalFlushRunning = false;
let flushBudgetStart = 0;
let currentTime = performance.now();
Expand Down Expand Up @@ -425,6 +426,10 @@ This is often caused by modifying a signal in an already rendered component duri
}

function applyJournalFlush() {
if (blockJournalFlush) {
DEBUG && debugTrace('journalFlush.BLOCKED', null, choreQueue, blockedChores);
return;
}
if (!isJournalFlushRunning) {
// prevent multiple journal flushes from running at the same time
isJournalFlushRunning = true;
Expand Down Expand Up @@ -681,11 +686,16 @@ This is often caused by modifying a signal in an already rendered component duri
host
) as ValueOrPromise<ChoreReturnValue<ChoreType.TASK>>;
} else {
returnValue = runTask(
payload as Task<TaskFn, TaskFn>,
container,
host
) as ValueOrPromise<ChoreReturnValue<ChoreType.TASK>>;
const task = payload as Task<TaskFn, TaskFn>;
returnValue = runTask(task, container, host) as ValueOrPromise<
ChoreReturnValue<ChoreType.TASK>
>;
if (task.$flags$ & TaskFlags.RENDER_BLOCKING) {
blockJournalFlush = true;
returnValue = maybeThen(returnValue, () => {
blockJournalFlush = false;
});
}
}
}
break;
Expand Down
84 changes: 82 additions & 2 deletions packages/qwik/src/core/tests/use-task.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,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';
Expand Down Expand Up @@ -652,6 +652,86 @@ 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++;
},
{
blockRender: true,
}
);
return (
<button
onClick$={() => {
count.value++;
}}
>
{text.value}
</button>
);
});

// Start rendering
const renderPromise = render(<Counter />, { debug });
// Advance timers to complete rendering
await vi.advanceTimersToNextTimerAsync();
const { document } = await renderPromise;

// Initial render
await expect(document.body.firstChild).toMatchDOM(<button>val1</button>);

// 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(<button>val1</button>);
// 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(<button>val2</button>);

// 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(<button>val2</button>);
// 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(<button>val3</button>);

vi.useRealTimers();
});
});

describe('regression', () => {
it('#5782', async () => {
const Child = component$(({ sig }: { sig: SignalType<SignalType<number>> }) => {
Expand Down Expand Up @@ -896,7 +976,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$(() => {
Expand Down
7 changes: 5 additions & 2 deletions packages/qwik/src/core/use/use-task-dollar.ts
Original file line number Diff line number Diff line change
@@ -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';

// <docs markdown="../readme.md#useTask">
// !!DO NOT EDIT THIS COMMENT DIRECTLY!!!
Expand Down Expand Up @@ -63,4 +63,7 @@ import { useTaskQrl, type TaskFn } from './use-task';
*/
// </docs>
// 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;
Loading