From 7cbfe1a6f723d41a1ab89408e266aa961daa0e3d Mon Sep 17 00:00:00 2001 From: Wout Mertens Date: Wed, 15 May 2024 18:42:26 +0200 Subject: [PATCH] feat(core): useConst, createSignal, createComputed$ --- .../docs/src/routes/api/qwik-city/api.json | 2 +- .../docs/src/routes/api/qwik-city/index.md | 4 +- packages/docs/src/routes/api/qwik/api.json | 96 ++++++- packages/docs/src/routes/api/qwik/index.md | 237 ++++++++++++++++-- packages/qwik-city/runtime/src/api.md | 2 +- packages/qwik/src/core/api.md | 41 +-- packages/qwik/src/core/index.ts | 17 +- packages/qwik/src/core/state/signal.ts | 13 +- packages/qwik/src/core/use/use-core.ts | 4 + packages/qwik/src/core/use/use-signal.ts | 49 +++- packages/qwik/src/core/use/use-task.ts | 50 ++-- .../qwik/src/core/util/implicit_dollar.ts | 4 +- .../e2e/src/components/signals/signals.tsx | 39 +++ starters/e2e/e2e.signals.spec.ts | 11 + 14 files changed, 477 insertions(+), 92 deletions(-) diff --git a/packages/docs/src/routes/api/qwik-city/api.json b/packages/docs/src/routes/api/qwik-city/api.json index ebab18cf9fe..bc7c34cb722 100644 --- a/packages/docs/src/routes/api/qwik-city/api.json +++ b/packages/docs/src/routes/api/qwik-city/api.json @@ -670,7 +670,7 @@ } ], "kind": "Function", - "content": "```typescript\nserver$: (first: T, options?: ServerConfig | undefined) => ServerQRL\n```\n\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nfirst\n\n\n\n\nT\n\n\n\n\n\n
\n\noptions\n\n\n\n\nServerConfig \\| undefined\n\n\n\n\n_(Optional)_\n\n\n
\n**Returns:**\n\n[ServerQRL](#serverqrl)<T>", + "content": "```typescript\nserver$: (qrl: T, options?: ServerConfig | undefined) => ServerQRL\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\nT\n\n\n\n\n\n
\n\noptions\n\n\n\n\nServerConfig \\| undefined\n\n\n\n\n_(Optional)_\n\n\n
\n**Returns:**\n\n[ServerQRL](#serverqrl)<T>", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/runtime/src/server-functions.ts", "mdFile": "qwik-city.server_.md" }, diff --git a/packages/docs/src/routes/api/qwik-city/index.md b/packages/docs/src/routes/api/qwik-city/index.md index 6e7a4e53303..f86d7b84012 100644 --- a/packages/docs/src/routes/api/qwik-city/index.md +++ b/packages/docs/src/routes/api/qwik-city/index.md @@ -2129,7 +2129,7 @@ RouterOutlet: import("@builder.io/qwik").Component; ```typescript server$: ( - first: T, + qrl: T, options?: ServerConfig | undefined, ) => ServerQRL; ``` @@ -2149,7 +2149,7 @@ Description -first +qrl diff --git a/packages/docs/src/routes/api/qwik/api.json b/packages/docs/src/routes/api/qwik/api.json index 00a2e7799a9..2b2e85a0cce 100644 --- a/packages/docs/src/routes/api/qwik/api.json +++ b/packages/docs/src/routes/api/qwik/api.json @@ -506,6 +506,20 @@ "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/component/component.public.ts", "mdFile": "qwik.componentqrl.md" }, + { + "name": "ComputedFn", + "id": "computedfn", + "hierarchy": [ + { + "name": "ComputedFn", + "id": "computedfn" + } + ], + "kind": "TypeAlias", + "content": "```typescript\nexport type ComputedFn = () => T;\n```", + "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-task.ts", + "mdFile": "qwik.computedfn.md" + }, { "name": "ContextId", "id": "contextid", @@ -548,6 +562,34 @@ "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/render/jsx/types/jsx-qwik-attributes.ts", "mdFile": "qwik.correctedtoggleevent.md" }, + { + "name": "createComputed$", + "id": "createcomputed_", + "hierarchy": [ + { + "name": "createComputed$", + "id": "createcomputed_" + } + ], + "kind": "Function", + "content": "Returns read-only signal that updates when signals used in the `ComputedFn` change. Unlike useComputed$, this is not a hook and it always creates a new signal.\n\n\n```typescript\ncreateComputed$: (qrl: ComputedFn) => Signal>\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nqrl\n\n\n\n\n[ComputedFn](#computedfn)<T>\n\n\n\n\n\n
\n**Returns:**\n\n[Signal](#signal)<Awaited<T>>", + "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-task.ts", + "mdFile": "qwik.createcomputed_.md" + }, + { + "name": "createComputedQrl", + "id": "createcomputedqrl", + "hierarchy": [ + { + "name": "createComputedQrl", + "id": "createcomputedqrl" + } + ], + "kind": "Function", + "content": "```typescript\ncreateComputedQrl: (qrl: QRL>) => Signal>\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nqrl\n\n\n\n\n[QRL](#qrl)<[ComputedFn](#computedfn)<T>>\n\n\n\n\n\n
\n**Returns:**\n\n[Signal](#signal)<Awaited<T>>", + "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-task.ts", + "mdFile": "qwik.createcomputedqrl.md" + }, { "name": "createContextId", "id": "createcontextid", @@ -562,6 +604,20 @@ "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-context.ts", "mdFile": "qwik.createcontextid.md" }, + { + "name": "createSignal", + "id": "createsignal", + "hierarchy": [ + { + "name": "createSignal", + "id": "createsignal" + } + ], + "kind": "Variable", + "content": "Creates a signal.\n\nIf the initial state is a function, the function is invoked to calculate the actual initial state.\n\n\n```typescript\ncreateSignal: UseSignal\n```", + "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-signal.ts", + "mdFile": "qwik.createsignal.md" + }, { "name": "CSSProperties", "id": "cssproperties", @@ -763,7 +819,7 @@ } ], "kind": "Function", - "content": "```typescript\nevent$: (first: T) => QRL\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nfirst\n\n\n\n\nT\n\n\n\n\n\n
\n**Returns:**\n\n[QRL](#qrl)<T>", + "content": "```typescript\nevent$: (qrl: T) => QRL\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\nT\n\n\n\n\n\n
\n**Returns:**\n\n[QRL](#qrl)<T>", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/qrl/qrl.public.ts", "mdFile": "qwik.event_.md" }, @@ -1071,7 +1127,7 @@ } ], "kind": "Function", - "content": "Create a `____$(...)` convenience method from `___(...)`.\n\nIt is very common for functions to take a lazy-loadable resource as a first argument. For this reason, the Qwik Optimizer automatically extracts the first argument from any function which ends in `$`.\n\nThis means that `foo$(arg0)` and `foo($(arg0))` are equivalent with respect to Qwik Optimizer. The former is just a shorthand for the latter.\n\nFor example, these function calls are equivalent:\n\n- `component$(() => {...})` is same as `component($(() => {...}))`\n\n```tsx\nexport function myApi(callback: QRL<() => void>): void {\n // ...\n}\n\nexport const myApi$ = implicit$FirstArg(myApi);\n// type of myApi$: (callback: () => void): void\n\n// can be used as:\nmyApi$(() => console.log('callback'));\n\n// will be transpiled to:\n// FILE: \nmyApi(qrl('./chunk-abc.js', 'callback'));\n\n// FILE: chunk-abc.js\nexport const callback = () => console.log('callback');\n```\n\n\n```typescript\nimplicit$FirstArg: (fn: (first: QRL, ...rest: REST) => RET) => ((first: FIRST, ...rest: REST) => RET)\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nfn\n\n\n\n\n(first: [QRL](#qrl)<FIRST>, ...rest: REST) => RET\n\n\n\n\nA function that should have its first argument automatically `$`.\n\n\n
\n**Returns:**\n\n((first: FIRST, ...rest: REST) => RET)", + "content": "Create a `____$(...)` convenience method from `___(...)`.\n\nIt is very common for functions to take a lazy-loadable resource as a first argument. For this reason, the Qwik Optimizer automatically extracts the first argument from any function which ends in `$`.\n\nThis means that `foo$(arg0)` and `foo($(arg0))` are equivalent with respect to Qwik Optimizer. The former is just a shorthand for the latter.\n\nFor example, these function calls are equivalent:\n\n- `component$(() => {...})` is same as `component($(() => {...}))`\n\n```tsx\nexport function myApi(callback: QRL<() => void>): void {\n // ...\n}\n\nexport const myApi$ = implicit$FirstArg(myApi);\n// type of myApi$: (callback: () => void): void\n\n// can be used as:\nmyApi$(() => console.log('callback'));\n\n// will be transpiled to:\n// FILE: \nmyApi(qrl('./chunk-abc.js', 'callback'));\n\n// FILE: chunk-abc.js\nexport const callback = () => console.log('callback');\n```\n\n\n```typescript\nimplicit$FirstArg: (fn: (qrl: QRL, ...rest: REST) => RET) => ((qrl: FIRST, ...rest: REST) => RET)\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nfn\n\n\n\n\n(qrl: [QRL](#qrl)<FIRST>, ...rest: REST) => RET\n\n\n\n\nA function that should have its first argument automatically `$`.\n\n\n
\n**Returns:**\n\n((qrl: FIRST, ...rest: REST) => RET)", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/util/implicit_dollar.ts", "mdFile": "qwik.implicit_firstarg.md" }, @@ -2474,7 +2530,7 @@ } ], "kind": "Interface", - "content": "```typescript\nexport interface Signal \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[value](#)\n\n\n\n\n\n\n\nT\n\n\n\n\n\n
", + "content": "A signal is a reactive value which can be read and written. When the signal is written, all tasks which are tracking the signal will be re-run and all components that read the signal will be re-rendered.\n\nFurthermore, when a signal value is passed as a prop to a component, the optimizer will automatically forward the signal. This means that `return
hi
` will update the `title` attribute when the signal changes without having to re-render the component.\n\n\n```typescript\nexport interface Signal \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[value](#)\n\n\n\n\n\n\n\nT\n\n\n\n\n\n
", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/state/signal.ts", "mdFile": "qwik.signal.md" }, @@ -2963,8 +3019,8 @@ "id": "usecomputed_" } ], - "kind": "Variable", - "content": "```typescript\nuseComputed$: Computed\n```", + "kind": "Function", + "content": "Hook that returns a read-only signal that updates when signals used in the `ComputedFn` change.\n\n\n```typescript\nuseComputed$: (qrl: ComputedFn) => Signal>\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nqrl\n\n\n\n\n[ComputedFn](#computedfn)<T>\n\n\n\n\n\n
\n**Returns:**\n\n[Signal](#signal)<Awaited<T>>", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-task.ts", "mdFile": "qwik.usecomputed_.md" }, @@ -2977,11 +3033,25 @@ "id": "usecomputedqrl" } ], - "kind": "Variable", - "content": "```typescript\nuseComputedQrl: ComputedQRL\n```", + "kind": "Function", + "content": "```typescript\nuseComputedQrl: (qrl: QRL>) => Signal>\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nqrl\n\n\n\n\n[QRL](#qrl)<[ComputedFn](#computedfn)<T>>\n\n\n\n\n\n
\n**Returns:**\n\n[Signal](#signal)<Awaited<T>>", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-task.ts", "mdFile": "qwik.usecomputedqrl.md" }, + { + "name": "useConst", + "id": "useconst", + "hierarchy": [ + { + "name": "useConst", + "id": "useconst" + } + ], + "kind": "Function", + "content": "Stores 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\nuseConst: (value: (() => T) | T) => T\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nvalue\n\n\n\n\n(() => T) \\| T\n\n\n\n\n\n
\n**Returns:**\n\nT", + "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-signal.ts", + "mdFile": "qwik.useconst.md" + }, { "name": "useContext", "id": "usecontext", @@ -3132,7 +3202,7 @@ } ], "kind": "Variable", - "content": "```typescript\nuseSignal: UseSignal\n```", + "content": "Hook that creates a signal that is retained for the lifetime of the component.\n\n\n```typescript\nuseSignal: UseSignal\n```", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-signal.ts", "mdFile": "qwik.usesignal.md" }, @@ -3146,7 +3216,7 @@ } ], "kind": "Interface", - "content": "```typescript\nuseSignal: UseSignal\n```", + "content": "Hook that creates a signal that is retained for the lifetime of the component.\n\n\n```typescript\nuseSignal: UseSignal\n```", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-signal.ts", "mdFile": "qwik.usesignal.md" }, @@ -3188,7 +3258,7 @@ } ], "kind": "Function", - "content": "A lazy-loadable reference to a component's styles.\n\nComponent styles allow Qwik to lazy load the style information for the component only when needed. (And avoid double loading it in case of SSR hydration.)\n\n```tsx\nimport styles from './code-block.css?inline';\n\nexport const CmpStyles = component$(() => {\n useStyles$(styles);\n\n return
Some text
;\n});\n```\n\n\n```typescript\nuseStyles$: (first: string) => void\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nfirst\n\n\n\n\nstring\n\n\n\n\n\n
\n**Returns:**\n\nvoid", + "content": "A lazy-loadable reference to a component's styles.\n\nComponent styles allow Qwik to lazy load the style information for the component only when needed. (And avoid double loading it in case of SSR hydration.)\n\n```tsx\nimport styles from './code-block.css?inline';\n\nexport const CmpStyles = component$(() => {\n useStyles$(styles);\n\n return
Some text
;\n});\n```\n\n\n```typescript\nuseStyles$: (qrl: string) => void\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\nstring\n\n\n\n\n\n
\n**Returns:**\n\nvoid", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-styles.ts", "mdFile": "qwik.usestyles_.md" }, @@ -3230,7 +3300,7 @@ } ], "kind": "Function", - "content": "A lazy-loadable reference to a component's styles, that is scoped to the component.\n\nComponent styles allow Qwik to lazy load the style information for the component only when needed. (And avoid double loading it in case of SSR hydration.)\n\n```tsx\nimport scoped from './code-block.css?inline';\n\nexport const CmpScopedStyles = component$(() => {\n useStylesScoped$(scoped);\n\n return
Some text
;\n});\n```\n\n\n```typescript\nuseStylesScoped$: (first: string) => UseStylesScoped\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nfirst\n\n\n\n\nstring\n\n\n\n\n\n
\n**Returns:**\n\n[UseStylesScoped](#usestylesscoped)", + "content": "A lazy-loadable reference to a component's styles, that is scoped to the component.\n\nComponent styles allow Qwik to lazy load the style information for the component only when needed. (And avoid double loading it in case of SSR hydration.)\n\n```tsx\nimport scoped from './code-block.css?inline';\n\nexport const CmpScopedStyles = component$(() => {\n useStylesScoped$(scoped);\n\n return
Some text
;\n});\n```\n\n\n```typescript\nuseStylesScoped$: (qrl: string) => UseStylesScoped\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\nstring\n\n\n\n\n\n
\n**Returns:**\n\n[UseStylesScoped](#usestylesscoped)", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-styles.ts", "mdFile": "qwik.usestylesscoped_.md" }, @@ -3258,7 +3328,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$: (first: TaskFn, opts?: UseTaskOptions | undefined) => void\n```\n\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nfirst\n\n\n\n\n[TaskFn](#taskfn)\n\n\n\n\n\n
\n\nopts\n\n\n\n\n[UseTaskOptions](#usetaskoptions) \\| undefined\n\n\n\n\n_(Optional)_\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$: (qrl: TaskFn, opts?: UseTaskOptions | undefined) => void\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[TaskFn](#taskfn)\n\n\n\n\n\n
\n\nopts\n\n\n\n\n[UseTaskOptions](#usetaskoptions) \\| undefined\n\n\n\n\n_(Optional)_\n\n\n
\n**Returns:**\n\nvoid", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-task.ts", "mdFile": "qwik.usetask_.md" }, @@ -3300,7 +3370,7 @@ } ], "kind": "Function", - "content": "```tsx\nconst Timer = component$(() => {\n const store = useStore({\n count: 0,\n });\n\n useVisibleTask$(() => {\n // Only runs in the client\n const timer = setInterval(() => {\n store.count++;\n }, 500);\n return () => {\n clearInterval(timer);\n };\n });\n\n return
{store.count}
;\n});\n```\n\n\n```typescript\nuseVisibleTask$: (first: TaskFn, opts?: OnVisibleTaskOptions | undefined) => void\n```\n\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nfirst\n\n\n\n\n[TaskFn](#taskfn)\n\n\n\n\n\n
\n\nopts\n\n\n\n\n[OnVisibleTaskOptions](#onvisibletaskoptions) \\| undefined\n\n\n\n\n_(Optional)_\n\n\n
\n**Returns:**\n\nvoid", + "content": "```tsx\nconst Timer = component$(() => {\n const store = useStore({\n count: 0,\n });\n\n useVisibleTask$(() => {\n // Only runs in the client\n const timer = setInterval(() => {\n store.count++;\n }, 500);\n return () => {\n clearInterval(timer);\n };\n });\n\n return
{store.count}
;\n});\n```\n\n\n```typescript\nuseVisibleTask$: (qrl: TaskFn, opts?: OnVisibleTaskOptions | undefined) => void\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[TaskFn](#taskfn)\n\n\n\n\n\n
\n\nopts\n\n\n\n\n[OnVisibleTaskOptions](#onvisibletaskoptions) \\| undefined\n\n\n\n\n_(Optional)_\n\n\n
\n**Returns:**\n\nvoid", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-task.ts", "mdFile": "qwik.usevisibletask_.md" }, diff --git a/packages/docs/src/routes/api/qwik/index.md b/packages/docs/src/routes/api/qwik/index.md index ac0e7dc6cf0..d47c2ee41ee 100644 --- a/packages/docs/src/routes/api/qwik/index.md +++ b/packages/docs/src/routes/api/qwik/index.md @@ -1408,6 +1408,14 @@ componentQrl [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/component/component.public.ts) +## ComputedFn + +```typescript +export type ComputedFn = () => T; +``` + +[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-task.ts) + ## ContextId ContextId is a typesafe ID for your context. @@ -1688,6 +1696,82 @@ Description [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/render/jsx/types/jsx-qwik-attributes.ts) +## createComputed$ + +Returns read-only signal that updates when signals used in the `ComputedFn` change. Unlike useComputed$, this is not a hook and it always creates a new signal. + +```typescript +createComputed$: (qrl: ComputedFn) => Signal>; +``` + + + +
+ +Parameter + + + +Type + + + +Description + +
+ +qrl + + + +[ComputedFn](#computedfn)<T> + + + +
+**Returns:** + +[Signal](#signal)<Awaited<T>> + +[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-task.ts) + +## createComputedQrl + +```typescript +createComputedQrl: (qrl: QRL>) => Signal>; +``` + + + +
+ +Parameter + + + +Type + + + +Description + +
+ +qrl + + + +[QRL](#qrl)<[ComputedFn](#computedfn)<T>> + + + +
+**Returns:** + +[Signal](#signal)<Awaited<T>> + +[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-task.ts) + ## createContextId Create a context ID to be used in your application. The name should be written with no spaces. @@ -1769,6 +1853,18 @@ The name of the context. [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-context.ts) +## createSignal + +Creates a signal. + +If the initial state is a function, the function is invoked to calculate the actual initial state. + +```typescript +createSignal: UseSignal; +``` + +[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-signal.ts) + ## CSSProperties ```typescript @@ -2061,7 +2157,7 @@ any \| undefined ## event$ ```typescript -event$: (first: T) => QRL; +event$: (qrl: T) => QRL; ```
@@ -2079,7 +2175,7 @@ Description
-first +qrl @@ -2571,9 +2667,9 @@ export const callback = () => console.log("callback"); ```typescript implicit$FirstArg: ( - fn: (first: QRL, ...rest: REST) => RET, + fn: (qrl: QRL, ...rest: REST) => RET, ) => - (first: FIRST, ...rest: REST) => + (qrl: FIRST, ...rest: REST) => RET; ``` @@ -2596,7 +2692,7 @@ fn -(first: [QRL](#qrl)<FIRST>, ...rest: REST) => RET +(qrl: [QRL](#qrl)<FIRST>, ...rest: REST) => RET @@ -2606,7 +2702,7 @@ A function that should have its first argument automatically `$`.
**Returns:** -((first: FIRST, ...rest: REST) => RET) +((qrl: FIRST, ...rest: REST) => RET) [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/util/implicit_dollar.ts) @@ -5223,6 +5319,10 @@ plt ## Signal +A signal is a reactive value which can be read and written. When the signal is written, all tasks which are tracking the signal will be re-run and all components that read the signal will be re-rendered. + +Furthermore, when a signal value is passed as a prop to a component, the optimizer will automatically forward the signal. This means that `return
hi
` will update the `title` attribute when the signal changes without having to re-render the component. + ```typescript export interface Signal ``` @@ -10009,20 +10109,121 @@ T ## useComputed$ +Hook that returns a read-only signal that updates when signals used in the `ComputedFn` change. + ```typescript -useComputed$: Computed; +useComputed$: (qrl: ComputedFn) => Signal>; ``` + + +
+ +Parameter + + + +Type + + + +Description + +
+ +qrl + + + +[ComputedFn](#computedfn)<T> + + + +
+**Returns:** + +[Signal](#signal)<Awaited<T>> + [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-task.ts) ## useComputedQrl ```typescript -useComputedQrl: ComputedQRL; +useComputedQrl: (qrl: QRL>) => Signal>; ``` + + +
+ +Parameter + + + +Type + + + +Description + +
+ +qrl + + + +[QRL](#qrl)<[ComputedFn](#computedfn)<T>> + + + +
+**Returns:** + +[Signal](#signal)<Awaited<T>> + [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-task.ts) +## useConst + +Stores a value which is retained for the lifetime of the component. + +If the value is a function, the function is invoked to calculate the actual value. + +```typescript +useConst: (value: (() => T) | T) => T; +``` + + + +
+ +Parameter + + + +Type + + + +Description + +
+ +value + + + +(() => T) \| T + + + +
+**Returns:** + +T + +[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-signal.ts) + ## useContext Retrieve Context value. @@ -10566,6 +10767,8 @@ T \| undefined ## useSignal +Hook that creates a signal that is retained for the lifetime of the component. + ```typescript useSignal: UseSignal; ``` @@ -10574,6 +10777,8 @@ useSignal: UseSignal; ## UseSignal +Hook that creates a signal that is retained for the lifetime of the component. + ```typescript useSignal: UseSignal; ``` @@ -10762,7 +10967,7 @@ export const CmpStyles = component$(() => { ``` ```typescript -useStyles$: (first: string) => void +useStyles$: (qrl: string) => void ```
@@ -10780,7 +10985,7 @@ Description
-first +qrl @@ -10904,7 +11109,7 @@ export const CmpScopedStyles = component$(() => { ``` ```typescript -useStylesScoped$: (first: string) => UseStylesScoped; +useStylesScoped$: (qrl: string) => UseStylesScoped; ```
@@ -10922,7 +11127,7 @@ Description
-first +qrl @@ -10998,7 +11203,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$: (first: TaskFn, opts?: UseTaskOptions | undefined) => void +useTask$: (qrl: TaskFn, opts?: UseTaskOptions | undefined) => void ```
@@ -11016,7 +11221,7 @@ Description
-first +qrl @@ -11166,7 +11371,7 @@ const Timer = component$(() => { ``` ```typescript -useVisibleTask$: (first: TaskFn, opts?: OnVisibleTaskOptions | undefined) => void +useVisibleTask$: (qrl: TaskFn, opts?: OnVisibleTaskOptions | undefined) => void ```
@@ -11184,7 +11389,7 @@ Description
-first +qrl diff --git a/packages/qwik-city/runtime/src/api.md b/packages/qwik-city/runtime/src/api.md index 4d561ad2670..0cc5a0c9fa0 100644 --- a/packages/qwik-city/runtime/src/api.md +++ b/packages/qwik-city/runtime/src/api.md @@ -415,7 +415,7 @@ export const RouterOutlet: Component; // Warning: (ae-forgotten-export) The symbol "ServerConfig" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export const server$: (first: T, options?: ServerConfig | undefined) => ServerQRL; +export const server$: (qrl: T, options?: ServerConfig | undefined) => ServerQRL; // @public (undocumented) export type ServerFunction = { diff --git a/packages/qwik/src/core/api.md b/packages/qwik/src/core/api.md index 05a4814c214..f919ddd6ebe 100644 --- a/packages/qwik/src/core/api.md +++ b/packages/qwik/src/core/api.md @@ -131,6 +131,9 @@ export interface ComponentBaseProps { // @public export const componentQrl: >(componentQrl: QRL>) => Component; +// @public (undocumented) +export type ComputedFn = () => T; + // @public export interface ContextId { readonly __brand_context_type__: STATE; @@ -154,9 +157,18 @@ export interface CorrectedToggleEvent extends Event { readonly prevState: 'open' | 'closed'; } +// @public +export const createComputed$: (qrl: ComputedFn) => Signal>; + +// @public (undocumented) +export const createComputedQrl: (qrl: QRL>) => Signal>; + // @public export const createContextId: (name: string) => ContextId; +// @public +export const createSignal: UseSignal; + // @public (undocumented) export interface CSSProperties extends CSS_2.Properties, CSS_2.PropertiesHyphen { [v: `--${string}`]: string | number | undefined; @@ -216,7 +228,7 @@ export interface ErrorBoundaryStore { } // @public (undocumented) -export const event$: (first: T) => QRL; +export const event$: (qrl: T) => QRL; // @public export type EventHandler = { @@ -342,7 +354,7 @@ export interface ImgHTMLAttributes extends Attrs<'img', T> { export const _IMMUTABLE: unique symbol; // @public -export const implicit$FirstArg: (fn: (first: QRL, ...rest: REST) => RET) => ((first: FIRST, ...rest: REST) => RET); +export const implicit$FirstArg: (fn: (qrl: QRL, ...rest: REST) => RET) => ((qrl: FIRST, ...rest: REST) => RET); // Warning: (ae-internal-missing-underscore) The name "inlinedQrl" should be prefixed with an underscore because the declaration is marked as @internal // @@ -903,7 +915,7 @@ export const _serializeData: (data: any, pureQRL?: boolean) => Promise; // @public export const setPlatform: (plt: CorePlatform) => CorePlatform; -// @public (undocumented) +// @public export interface Signal { // (undocumented) value: T; @@ -1609,15 +1621,14 @@ export interface TrackHTMLAttributes extends Attrs<'track', T // @public export const untrack: (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$: (qrl: ComputedFn) => Signal>; -// 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: (qrl: QRL>) => Signal>; + +// @public +export const useConst: (value: (() => T) | T) => T; // Warning: (ae-forgotten-export) The symbol "UseContext" needs to be exported by the entry point index.d.ts // @@ -1669,7 +1680,7 @@ export interface UseSignal { (value: T | (() => T)): Signal; } -// @public (undocumented) +// @public export const useSignal: UseSignal; // @public @@ -1682,13 +1693,13 @@ export interface UseStoreOptions { } // @public -export const useStyles$: (first: string) => void; +export const useStyles$: (qrl: string) => void; // @public export const useStylesQrl: (styles: QRL) => void; // @public -export const useStylesScoped$: (first: string) => UseStylesScoped; +export const useStylesScoped$: (qrl: string) => UseStylesScoped; // @public (undocumented) export interface UseStylesScoped { @@ -1700,7 +1711,7 @@ export interface UseStylesScoped { export const useStylesScopedQrl: (styles: QRL) => UseStylesScoped; // @public -export const useTask$: (first: TaskFn, opts?: UseTaskOptions | undefined) => void; +export const useTask$: (qrl: TaskFn, opts?: UseTaskOptions | undefined) => void; // @public (undocumented) export interface UseTaskOptions { @@ -1711,7 +1722,7 @@ export interface UseTaskOptions { export const useTaskQrl: (qrl: QRL, opts?: UseTaskOptions) => void; // @public -export const useVisibleTask$: (first: TaskFn, opts?: OnVisibleTaskOptions | undefined) => void; +export const useVisibleTask$: (qrl: TaskFn, opts?: OnVisibleTaskOptions | undefined) => void; // @public export const useVisibleTaskQrl: (qrl: QRL, opts?: OnVisibleTaskOptions) => void; diff --git a/packages/qwik/src/core/index.ts b/packages/qwik/src/core/index.ts index fbf964b69ad..abfd39b06ac 100644 --- a/packages/qwik/src/core/index.ts +++ b/packages/qwik/src/core/index.ts @@ -87,7 +87,7 @@ export { useContext, useContextProvider, createContextId } from './use/use-conte export { useServerData } from './use/use-env-data'; export { useStylesQrl, useStyles$, useStylesScopedQrl, useStylesScoped$ } from './use/use-styles'; export { useOn, useOnDocument, useOnWindow } from './use/use-on'; -export { useSignal } from './use/use-signal'; +export { useSignal, useConst, createSignal } from './use/use-signal'; export { withLocale, getLocale } from './use/use-locale'; export type { UseStylesScoped } from './use/use-styles'; @@ -95,25 +95,26 @@ export type { UseSignal } from './use/use-signal'; export type { ContextId } from './use/use-context'; export type { UseStoreOptions } from './use/use-store.public'; export type { - Tracker, - TaskFn, - OnVisibleTaskOptions, - VisibleTaskStrategy, + ComputedFn, EagernessOptions, - ResourceReturn, + OnVisibleTaskOptions, ResourceCtx, + ResourceFn, ResourcePending, ResourceRejected, ResourceResolved, + ResourceReturn, TaskCtx, + TaskFn, + Tracker, UseTaskOptions, - ResourceFn, + VisibleTaskStrategy, } from './use/use-task'; export type { ResourceProps, ResourceOptions } from './use/use-resource'; export { useResource$, useResourceQrl, Resource } from './use/use-resource'; export { useTask$, useTaskQrl } from './use/use-task'; export { useVisibleTask$, useVisibleTaskQrl } from './use/use-task'; -export { useComputed$, useComputedQrl } from './use/use-task'; +export { useComputed$, useComputedQrl, createComputed$, createComputedQrl } from './use/use-task'; export { useErrorBoundary } from './use/use-error-boundary'; export type { ErrorBoundaryStore } from './render/error-handling'; diff --git a/packages/qwik/src/core/state/signal.ts b/packages/qwik/src/core/state/signal.ts index 50342cd36ec..e26e7528207 100644 --- a/packages/qwik/src/core/state/signal.ts +++ b/packages/qwik/src/core/state/signal.ts @@ -15,7 +15,18 @@ import { import { QObjectManagerSymbol, _IMMUTABLE, _IMMUTABLE_PREFIX } from './constants'; import { _fnSignal } from '../qrl/inlined-fn'; -/** @public */ +/** + * A signal is a reactive value which can be read and written. When the signal is written, all tasks + * which are tracking the signal will be re-run and all components that read the signal will be + * re-rendered. + * + * Furthermore, when a signal value is passed as a prop to a component, the optimizer will + * automatically forward the signal. This means that `return
hi
` + * will update the `title` attribute when the signal changes without having to re-render the + * component. + * + * @public + */ export interface Signal { value: T; } diff --git a/packages/qwik/src/core/use/use-core.ts b/packages/qwik/src/core/use/use-core.ts index a1358ceb448..0b7d9b81a7e 100644 --- a/packages/qwik/src/core/use/use-core.ts +++ b/packages/qwik/src/core/use/use-core.ts @@ -123,6 +123,10 @@ export const useInvokeContext = (): RenderInvokeContext => { return ctx as RenderInvokeContext; }; +export const useContainerState = () => { + const ctx = useInvokeContext(); + return ctx.$renderCtx$.$static$.$containerState$; +}; export function useBindInvokeContext any>( this: unknown, diff --git a/packages/qwik/src/core/use/use-signal.ts b/packages/qwik/src/core/use/use-signal.ts index cea7e810b3e..ea30cacd350 100644 --- a/packages/qwik/src/core/use/use-signal.ts +++ b/packages/qwik/src/core/use/use-signal.ts @@ -1,7 +1,7 @@ import { isQwikComponent } from '../component/component.public'; import { _createSignal, type Signal } from '../state/signal'; import { isFunction } from '../util/types'; -import { invoke } from './use-core'; +import { invoke, useContainerState } from './use-core'; import { useSequentialScope } from './use-sequential-scope'; /** @public */ @@ -10,18 +10,45 @@ export interface UseSignal { (value: T | (() => T)): Signal; } -/** @public */ -export const useSignal: UseSignal = (initialState?: STATE): Signal => { - const { val, set, iCtx } = useSequentialScope>(); - if (val != null) { - return val; - } - - const containerState = iCtx.$renderCtx$.$static$.$containerState$; +/** + * Creates a signal. + * + * If the initial state is a function, the function is invoked to calculate the actual initial + * state. + * + * @public + */ +export const createSignal: UseSignal = (initialState?: STATE): Signal => { + const containerState = useContainerState(); const value = isFunction(initialState) && !isQwikComponent(initialState) ? invoke(undefined, initialState as any) : initialState; - const signal = _createSignal(value, containerState, 0, undefined) as Signal; - return set(signal); + return _createSignal(value, containerState, 0) as Signal; +}; + +/** + * Stores a value which is retained for the lifetime of the component. + * + * If the value is a function, the function is invoked to calculate the actual value. + * + * @public + */ +export const useConst = (value: (() => T) | T): T => { + const { val, set } = useSequentialScope(); + if (val != null) { + return val; + } + // Note: We are not using `invoke` here because we don't want to clear the context + value = isFunction(value) && !isQwikComponent(value) ? value() : value; + return set(value as T); +}; + +/** + * Hook that creates a signal that is retained for the lifetime of the component. + * + * @public + */ +export const useSignal: UseSignal = (initialState?: any) => { + return useConst(() => createSignal(initialState)); }; diff --git a/packages/qwik/src/core/use/use-task.ts b/packages/qwik/src/core/use/use-task.ts index 9f183db7540..d05eb84005e 100644 --- a/packages/qwik/src/core/use/use-task.ts +++ b/packages/qwik/src/core/use/use-task.ts @@ -1,4 +1,4 @@ -import { newInvokeContext, invoke, waitAndRun, untrack } from './use-core'; +import { newInvokeContext, invoke, waitAndRun, untrack, useInvokeContext } from './use-core'; import { logError, logErrorAndStop } from '../util/log'; import { delay, safeCall, maybeThen } from '../util/promises'; import { isFunction, isObject, type ValueOrPromise } from '../util/types'; @@ -33,6 +33,8 @@ import { } from '../state/signal'; import { QObjectManagerSymbol } from '../state/constants'; import { ComputedEvent, TaskEvent } from '../util/markers'; +import { getContext } from '../state/context'; +import { useConst } from './use-signal'; export const TaskFlagsIsVisibleTask = 1 << 0; export const TaskFlagsIsTask = 1 << 1; @@ -296,22 +298,13 @@ export const useTaskQrl = (qrl: QRL, opts?: UseTaskOptions): void => { } }; -interface ComputedQRL { - (qrl: QRL>): ReadonlySignal>; -} - -interface Computed { - (qrl: ComputedFn): ReadonlySignal>; -} - /** @public */ -export const useComputedQrl: ComputedQRL = (qrl: QRL>): Signal> => { - const { val, set, iCtx, i, elCtx } = useSequentialScope>>(); - if (val) { - return val; - } +export const createComputedQrl = (qrl: QRL>): Signal> => { assertQrl(qrl); + const iCtx = useInvokeContext(); + const hostElement = iCtx.$hostElement$; const containerState = iCtx.$renderCtx$.$static$.$containerState$; + const elCtx = getContext(hostElement, containerState); const signal = _createSignal( undefined as Awaited, containerState, @@ -321,23 +314,36 @@ export const useComputedQrl: ComputedQRL = (qrl: QRL>): Signal< const task = new Task( TaskFlagsIsDirty | TaskFlagsIsTask | TaskFlagsIsComputed, - i, + // Computed signals should update immediately + 0, elCtx.$element$, qrl, signal ); qrl.$resolveLazy$(containerState.$containerEl$); - if (!elCtx.$tasks$) { - elCtx.$tasks$ = []; - } - elCtx.$tasks$.push(task); + (elCtx.$tasks$ ||= []).push(task); waitAndRun(iCtx, () => runComputed(task, containerState, iCtx.$renderCtx$)); - return set(signal); + return signal as ReadonlySignal>; }; - /** @public */ -export const useComputed$: Computed = implicit$FirstArg(useComputedQrl); +export const useComputedQrl = (qrl: QRL>): Signal> => { + return useConst(() => createComputedQrl(qrl)); +}; + +/** + * Hook that returns a read-only signal that updates when signals used in the `ComputedFn` change. + * + * @public + */ +export const useComputed$ = implicit$FirstArg(useComputedQrl); +/** + * Returns read-only signal that updates when signals used in the `ComputedFn` change. Unlike + * useComputed$, this is not a hook and it always creates a new signal. + * + * @public + */ +export const createComputed$ = implicit$FirstArg(createComputedQrl); // // !!DO NOT EDIT THIS COMMENT DIRECTLY!!! diff --git a/packages/qwik/src/core/util/implicit_dollar.ts b/packages/qwik/src/core/util/implicit_dollar.ts index 043b0086531..3bfa0b31d3b 100644 --- a/packages/qwik/src/core/util/implicit_dollar.ts +++ b/packages/qwik/src/core/util/implicit_dollar.ts @@ -41,8 +41,8 @@ import { $, type QRL } from '../qrl/qrl.public'; */ // export const implicit$FirstArg = ( - fn: (first: QRL, ...rest: REST) => RET -): ((first: FIRST, ...rest: REST) => RET) => { + fn: (qrl: QRL, ...rest: REST) => RET +): ((qrl: FIRST, ...rest: REST) => RET) => { return function (first: FIRST, ...rest: REST): RET { return fn.call(null, $(first), ...rest); }; diff --git a/starters/apps/e2e/src/components/signals/signals.tsx b/starters/apps/e2e/src/components/signals/signals.tsx index f082da3f0b9..cf0c1dc8205 100644 --- a/starters/apps/e2e/src/components/signals/signals.tsx +++ b/starters/apps/e2e/src/components/signals/signals.tsx @@ -2,6 +2,8 @@ import { component$, type Signal, useSignal, + createSignal, + useConst, useStore, useVisibleTask$, useTask$, @@ -11,6 +13,7 @@ import { type QwikIntrinsicElements, Resource, useComputed$, + createComputed$, } from "@builder.io/qwik"; import { delay } from "../resource/resource"; import { @@ -134,6 +137,7 @@ export const SignalsChildren = component$(() => { + ); }); @@ -1238,3 +1242,38 @@ export const Issue4868Card = component$((props: { src: string }) => { ); }); + +export const ManySignals = component$(() => { + const signals = useConst(() => { + const arr: (Signal | string)[] = []; + for (let i = 0; i < 10; i++) { + arr.push(createSignal(0)); + arr.push(", "); + } + return arr; + }); + const doubles = useConst(() => + signals.map((s) => + typeof s === "string" ? s : createComputed$(() => s.value * 2), + ), + ); + + return ( + <> + +
{signals}
+
{doubles}
+ + ); +}); diff --git a/starters/e2e/e2e.signals.spec.ts b/starters/e2e/e2e.signals.spec.ts index 5e9e9fbc3eb..56fe1e5cff1 100644 --- a/starters/e2e/e2e.signals.spec.ts +++ b/starters/e2e/e2e.signals.spec.ts @@ -555,6 +555,17 @@ test.describe("signals", () => { `Card useComputed$: https://placehold.co/400x400?text=1&useComputed$`, ); }); + + test("createSignal/createComputed$", async ({ page }) => { + const button = page.locator("#many-signals-button"); + const result = page.locator("#many-signals-result"); + const doubles = page.locator("#many-doubles-result"); + await expect(result).toHaveText("0, 0, 0, 0, 0, 0, 0, 0, 0, 0, "); + await expect(doubles).toHaveText("0, 0, 0, 0, 0, 0, 0, 0, 0, 0, "); + await button.click(); + await expect(result).toHaveText("1, 1, 1, 1, 1, 1, 1, 1, 1, 1, "); + await expect(doubles).toHaveText("2, 2, 2, 2, 2, 2, 2, 2, 2, 2, "); + }); } tests();