Skip to content

Commit

Permalink
fix(ComputedSignal): qrl handling, disallow async results
Browse files Browse the repository at this point in the history
  • Loading branch information
wmertens committed Sep 24, 2024
1 parent e7aee77 commit 00c599d
Show file tree
Hide file tree
Showing 14 changed files with 226 additions and 90 deletions.
5 changes: 5 additions & 0 deletions .changeset/five-kangaroos-matter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@builder.io/qwik': patch
---

BREAKING: `useComputed$` no longer accepts Promise results. Instead, use `useSignal` and `useTask$` together to perform async signal updates
12 changes: 7 additions & 5 deletions packages/docs/src/repl/repl-output-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { component$ } from '@builder.io/qwik';
import { component$, useComputed$ } from '@builder.io/qwik';
import { CodeBlock } from '../components/code-block/code-block';
import { ReplOutputModules } from './repl-output-modules';
import { ReplOutputSymbols } from './repl-output-symbols';
Expand All @@ -7,7 +7,9 @@ import { ReplTabButtons } from './repl-tab-buttons';
import type { ReplAppInput, ReplStore } from './types';

export const ReplOutputPanel = component$(({ input, store }: ReplOutputPanelProps) => {
const diagnosticsLen = store.diagnostics.length + store.monacoDiagnostics.length;
const diagnosticsLen = useComputed$(
() => store.diagnostics.length + store.monacoDiagnostics.length
);

return (
<div class="repl-panel repl-output-panel">
Expand Down Expand Up @@ -61,8 +63,8 @@ export const ReplOutputPanel = component$(({ input, store }: ReplOutputPanelProp
) : null}

<ReplTabButton
text={`Diagnostics${diagnosticsLen > 0 ? ` (${diagnosticsLen})` : ``}`}
cssClass={{ 'repl-tab-diagnostics': true, 'has-errors': diagnosticsLen > 0 }}
text={`Diagnostics${diagnosticsLen.value > 0 ? ` (${diagnosticsLen.value})` : ``}`}
cssClass={{ 'repl-tab-diagnostics': true, 'has-errors': diagnosticsLen.value > 0 }}
isActive={store.selectedOutputPanel === 'diagnostics'}
onClick$={async () => {
store.selectedOutputPanel = 'diagnostics';
Expand Down Expand Up @@ -120,7 +122,7 @@ export const ReplOutputPanel = component$(({ input, store }: ReplOutputPanelProp

{store.selectedOutputPanel === 'diagnostics' ? (
<div class="output-result output-diagnostics">
{diagnosticsLen === 0 ? (
{diagnosticsLen.value === 0 ? (
<p class="no-diagnostics">- No Reported Diagnostics -</p>
) : (
[...store.diagnostics, ...store.monacoDiagnostics].map((d, key) => (
Expand Down
18 changes: 9 additions & 9 deletions packages/docs/src/routes/api/qwik/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,7 @@
}
],
"kind": "Interface",
"content": "```typescript\nexport interface ComputedSignal<T> extends ReadonlySignal<T> \n```\n**Extends:** [ReadonlySignal](#readonlysignal)<!-- -->&lt;T&gt;\n\n\n<table><thead><tr><th>\n\nMethod\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\n[force()](#computedsignal-force)\n\n\n</td><td>\n\nUse this to force recalculation and running subscribers, for example when the calculated value mutates but remains the same object. Useful for third-party libraries.\n\n\n</td></tr>\n</tbody></table>",
"content": "A computed signal is a signal which is calculated from other signals. When the signals change, the computed signal is recalculated, and if the result changed, all tasks which are tracking the signal will be re-run and all components that read the signal will be re-rendered.\n\n\n```typescript\nexport interface ComputedSignal<T> extends ReadonlySignal<T> \n```\n**Extends:** [ReadonlySignal](#readonlysignal)<!-- -->&lt;T&gt;\n\n\n<table><thead><tr><th>\n\nMethod\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\n[force()](#computedsignal-force)\n\n\n</td><td>\n\nUse this to force recalculation and running subscribers, for example when the calculated value mutates but remains the same object. Useful for third-party libraries.\n\n\n</td></tr>\n</tbody></table>",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/v2/signal/v2-signal.public.ts",
"mdFile": "qwik.computedsignal.md"
},
Expand Down Expand Up @@ -586,7 +586,7 @@
}
],
"kind": "Function",
"content": "```typescript\ncreateComputed$: <T>(qrl: () => T) => ComputedSignal<T>\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\nqrl\n\n\n</td><td>\n\n() =&gt; T\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>\n**Returns:**\n\n[ComputedSignal](#computedsignal)<!-- -->&lt;T&gt;",
"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 mus be synchronous.\n\nIf you need the function to be async, use `useSignal` and `useTask$` instead.\n\n\n```typescript\ncreateComputed$: <T>(qrl: () => T) => T extends Promise<any> ? never : ComputedSignal<T>\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\nqrl\n\n\n</td><td>\n\n() =&gt; T\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>\n**Returns:**\n\nT extends Promise&lt;any&gt; ? never : [ComputedSignal](#computedsignal)<!-- -->&lt;T&gt;",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/v2/signal/v2-signal.public.ts",
"mdFile": "qwik.createcomputed_.md"
},
Expand All @@ -600,7 +600,7 @@
}
],
"kind": "Function",
"content": "```typescript\ncreateComputedQrl: <T>(qrl: QRL<() => T>) => ComputedSignal<T>\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\nqrl\n\n\n</td><td>\n\n[QRL](#qrl)<!-- -->&lt;() =&gt; T&gt;\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>\n**Returns:**\n\n[ComputedSignal](#computedsignal)<!-- -->&lt;T&gt;",
"content": "```typescript\ncreateComputedQrl: <T>(qrl: QRL<() => T>) => T extends Promise<any> ? never : ComputedSignal<T>\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\nqrl\n\n\n</td><td>\n\n[QRL](#qrl)<!-- -->&lt;() =&gt; T&gt;\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>\n**Returns:**\n\nT extends Promise&lt;any&gt; ? never : [ComputedSignal](#computedsignal)<!-- -->&lt;T&gt;",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/v2/signal/v2-signal.public.ts",
"mdFile": "qwik.createcomputedqrl.md"
},
Expand Down Expand Up @@ -628,7 +628,7 @@
}
],
"kind": "Variable",
"content": "```typescript\ncreateSignal: {\n <T>(): Signal<T | undefined>;\n <T>(value: T): Signal<T>;\n}\n```",
"content": "Creates a Signal with the given value. If no value is given, the signal is created with `undefined`<!-- -->.\n\n\n```typescript\ncreateSignal: {\n <T>(): Signal<T | undefined>;\n <T>(value: T): Signal<T>;\n}\n```",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/v2/signal/v2-signal.public.ts",
"mdFile": "qwik.createsignal.md"
},
Expand Down Expand Up @@ -3036,8 +3036,8 @@
"id": "usecomputed_"
}
],
"kind": "Variable",
"content": "```typescript\nuseComputed$: Computed\n```",
"kind": "Function",
"content": "Creates a computed signal which is calculated from the given function. A computed signal is a signal which is calculated from other signals. When the signals change, the computed signal is recalculated, and if the result changed, all tasks which are tracking the signal will be re-run and all components that read the signal will be re-rendered.\n\nThe function must be synchronous and must not have any side effects.\n\n\n```typescript\nuseComputed$: <T>(qrl: import(\"./use-task\").ComputedFn<T>) => T extends Promise<any> ? never : import(\"..\").ReadonlySignal<T>\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\nqrl\n\n\n</td><td>\n\nimport(\"./use-task\").[ComputedFn](#computedfn)<!-- -->&lt;T&gt;\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>\n**Returns:**\n\nT extends Promise&lt;any&gt; ? never : import(\"..\").[ReadonlySignal](#readonlysignal)<!-- -->&lt;T&gt;",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-task-dollar.ts",
"mdFile": "qwik.usecomputed_.md"
},
Expand All @@ -3050,8 +3050,8 @@
"id": "usecomputedqrl"
}
],
"kind": "Variable",
"content": "```typescript\nuseComputedQrl: ComputedQRL\n```",
"kind": "Function",
"content": "```typescript\nuseComputedQrl: <T>(qrl: QRL<ComputedFn<T>>) => T extends Promise<any> ? never : ReadonlySignal<T>\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\nqrl\n\n\n</td><td>\n\n[QRL](#qrl)<!-- -->&lt;[ComputedFn](#computedfn)<!-- -->&lt;T&gt;&gt;\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>\n**Returns:**\n\nT extends Promise&lt;any&gt; ? never : [ReadonlySignal](#readonlysignal)<!-- -->&lt;T&gt;",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-task.ts",
"mdFile": "qwik.usecomputedqrl.md"
},
Expand All @@ -3065,7 +3065,7 @@
}
],
"kind": "Function",
"content": "> Warning: This API is now obsolete.\n> \n> This is a technology preview\n> \n\nStores a value which is retained for the lifetime of the component.\n\nIf the value is a function, the function is invoked to calculate the actual value.\n\n\n```typescript\nuseConstant: <T>(value: (() => T) | T) => T\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\nvalue\n\n\n</td><td>\n\n(() =&gt; T) \\| T\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>\n**Returns:**\n\nT",
"content": "Stores a value which is retained for the lifetime of the component. Subsequent calls to `useConstant` will always return the first value given.\n\nIf the value is a function, the function is invoked once to calculate the actual value.\n\n\n```typescript\nuseConstant: <T>(value: (() => T) | T) => T\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\nvalue\n\n\n</td><td>\n\n(() =&gt; T) \\| T\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>\n**Returns:**\n\nT",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-signal.ts",
"mdFile": "qwik.useconstant.md"
},
Expand Down
92 changes: 80 additions & 12 deletions packages/docs/src/routes/api/qwik/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -1418,6 +1418,8 @@ export type ComputedFn<T> = () => T;
## ComputedSignal
A computed signal is a signal which is calculated from other signals. When the signals change, the computed signal is recalculated, and if the result changed, all tasks which are tracking the signal will be re-run and all components that read the signal will be re-rendered.
```typescript
export interface ComputedSignal<T> extends ReadonlySignal<T>
```
Expand Down Expand Up @@ -1728,8 +1730,14 @@ Description
## createComputed$
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.
The QRL must be a function which returns the value of the signal. The function must not have side effects, and it mus be synchronous.
If you need the function to be async, use `useSignal` and `useTask$` instead.
```typescript
createComputed$: <T>(qrl: () => T) => ComputedSignal<T>;
createComputed$: <T>(qrl: () => T) => T extends Promise<any> ? never : ComputedSignal<T>
```
<table><thead><tr><th>
Expand Down Expand Up @@ -1759,14 +1767,14 @@ qrl
</tbody></table>
**Returns:**
[ComputedSignal](#computedsignal)&lt;T&gt;
T extends Promise&lt;any&gt; ? never : [ComputedSignal](#computedsignal)&lt;T&gt;
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/v2/signal/v2-signal.public.ts)
## createComputedQrl
```typescript
createComputedQrl: <T>(qrl: QRL<() => T>) => ComputedSignal<T>;
createComputedQrl: <T>(qrl: QRL<() => T>) => T extends Promise<any> ? never : ComputedSignal<T>
```
<table><thead><tr><th>
Expand Down Expand Up @@ -1796,7 +1804,7 @@ qrl
</tbody></table>
**Returns:**
[ComputedSignal](#computedsignal)&lt;T&gt;
T extends Promise&lt;any&gt; ? never : [ComputedSignal](#computedsignal)&lt;T&gt;
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/v2/signal/v2-signal.public.ts)
Expand Down Expand Up @@ -1883,6 +1891,8 @@ The name of the context.
## createSignal
Creates a Signal with the given value. If no value is given, the signal is created with `undefined`.
```typescript
createSignal: {
<T>(): Signal<T | undefined>;
Expand Down Expand Up @@ -10161,29 +10171,87 @@ T
## useComputed$
Creates a computed signal which is calculated from the given function. A computed signal is a signal which is calculated from other signals. When the signals change, the computed signal is recalculated, and if the result changed, all tasks which are tracking the signal will be re-run and all components that read the signal will be re-rendered.
The function must be synchronous and must not have any side effects.
```typescript
useComputed$: Computed;
useComputed$: <T>(qrl: import("./use-task").ComputedFn<T>) => T extends Promise<any> ? never : import("..").ReadonlySignal<T>
```
<table><thead><tr><th>
Parameter
</th><th>
Type
</th><th>
Description
</th></tr></thead>
<tbody><tr><td>
qrl
</td><td>
import("./use-task").[ComputedFn](#computedfn)&lt;T&gt;
</td><td>
</td></tr>
</tbody></table>
**Returns:**
T extends Promise&lt;any&gt; ? never : import("..").[ReadonlySignal](#readonlysignal)&lt;T&gt;
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-task-dollar.ts)
## useComputedQrl
```typescript
useComputedQrl: ComputedQRL;
useComputedQrl: <T>(qrl: QRL<ComputedFn<T>>) => T extends Promise<any> ? never : ReadonlySignal<T>
```
<table><thead><tr><th>
Parameter
</th><th>
Type
</th><th>
Description
</th></tr></thead>
<tbody><tr><td>
qrl
</td><td>
[QRL](#qrl)&lt;[ComputedFn](#computedfn)&lt;T&gt;&gt;
</td><td>
</td></tr>
</tbody></table>
**Returns:**
T extends Promise&lt;any&gt; ? never : [ReadonlySignal](#readonlysignal)&lt;T&gt;
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-task.ts)
## useConstant
> Warning: This API is now obsolete.
>
> This is a technology preview
Stores a value which is retained for the lifetime of the component.
Stores a value which is retained for the lifetime of the component. Subsequent calls to `useConstant` will always return the first value given.
If the value is a function, the function is invoked to calculate the actual value.
If the value is a function, the function is invoked once to calculate the actual value.
```typescript
useConstant: <T>(value: (() => T) | T) => T;
Expand Down
22 changes: 9 additions & 13 deletions packages/qwik/src/core/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ export const componentQrl: <PROPS extends Record<any, any>>(componentQrl: QRL<On
// @public (undocumented)
export type ComputedFn<T> = () => T;

// @public (undocumented)
// @public
export interface ComputedSignal<T> extends ReadonlySignal<T> {
force(): void;
}
Expand Down Expand Up @@ -205,16 +205,16 @@ export interface CorrectedToggleEvent extends Event {
readonly prevState: 'open' | 'closed';
}

// @public (undocumented)
export const createComputed$: <T>(qrl: () => T) => ComputedSignal<T>;
// @public
export const createComputed$: <T>(qrl: () => T) => T extends Promise<any> ? never : ComputedSignal<T>;

// @public (undocumented)
export const createComputedQrl: <T>(qrl: QRL<() => T>) => ComputedSignal<T>;
export const createComputedQrl: <T>(qrl: QRL<() => T>) => T extends Promise<any> ? never : ComputedSignal<T>;

// @public
export const createContextId: <STATE = unknown>(name: string) => ContextId<STATE>;

// @public (undocumented)
// @public
export const createSignal: {
<T>(): Signal<T | undefined>;
<T>(value: T): Signal<T>;
Expand Down Expand Up @@ -1880,17 +1880,13 @@ export interface TrackHTMLAttributes<T extends Element> extends Attrs<'track', T
// @public
export const untrack: <T>(fn: () => T) => T;

// Warning: (ae-forgotten-export) The symbol "Computed" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
export const useComputed$: Computed;
// @public
export const useComputed$: <T>(qrl: ComputedFn<T>) => T extends Promise<any> ? never : ReadonlySignal<T>;

// Warning: (ae-forgotten-export) The symbol "ComputedQRL" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
export const useComputedQrl: ComputedQRL;
export const useComputedQrl: <T>(qrl: QRL<ComputedFn<T>>) => T extends Promise<any> ? never : ReadonlySignal<T>;

// @public @deprecated
// @public
export const useConstant: <T>(value: (() => T) | T) => T;

// Warning: (ae-forgotten-export) The symbol "UseContext" needs to be exported by the entry point index.d.ts
Expand Down
24 changes: 10 additions & 14 deletions packages/qwik/src/core/use/use-signal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,21 @@ export interface UseSignal {

/** @public */
export const useSignal: UseSignal = <STATE>(initialState?: STATE): Signal<STATE> => {
const { val, set } = useSequentialScope<Signal<STATE>>();
if (val != null) {
return val;
}

const value =
isFunction(initialState) && !isQwikComponent(initialState)
? invoke(undefined, initialState as any)
: initialState;
const signal = createSignal<STATE>(value);
return set(signal);
return useConstant(() => {
const value =
isFunction(initialState) && !isQwikComponent(initialState)
? invoke(undefined, initialState as any)
: initialState;
return createSignal<STATE>(value);
});
};

/**
* Stores a value which is retained for the lifetime of the component.
* Stores a value which is retained for the lifetime of the component. Subsequent calls to
* `useConstant` will always return the first value given.
*
* If the value is a function, the function is invoked to calculate the actual value.
* If the value is a function, the function is invoked once to calculate the actual value.
*
* @deprecated This is a technology preview
* @public
*/
export const useConstant = <T>(value: (() => T) | T): T => {
Expand Down
20 changes: 12 additions & 8 deletions packages/qwik/src/core/use/use-task-dollar.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { implicit$FirstArg } from '../util/implicit_dollar';
import type { ReadonlySignal } from '../v2/signal/v2-signal.public';
import { useComputedQrl, useTaskQrl, useVisibleTaskQrl, type ComputedFn } from './use-task';
import { useComputedQrl, useTaskQrl, useVisibleTaskQrl } from './use-task';

interface Computed {
<T>(qrl: ComputedFn<T>): ReadonlySignal<T>;
}

/** @public */
export const useComputed$: Computed = implicit$FirstArg(useComputedQrl);
/**
* Creates a computed signal which is calculated from the given function. A computed signal is a
* signal which is calculated from other signals. When the signals change, the computed signal is
* recalculated, and if the result changed, all tasks which are tracking the signal will be re-run
* and all components that read the signal will be re-rendered.
*
* The function must be synchronous and must not have any side effects.
*
* @public
*/
export const useComputed$ = implicit$FirstArg(useComputedQrl);

// <docs markdown="../readme.md#useTask">
// !!DO NOT EDIT THIS COMMENT DIRECTLY!!!
Expand Down
Loading

0 comments on commit 00c599d

Please sign in to comment.