Skip to content

Commit e4a0e7a

Browse files
committed
feat(core): use/createSerialized$
1 parent 5a8c96e commit e4a0e7a

File tree

14 files changed

+453
-14
lines changed

14 files changed

+453
-14
lines changed

.changeset/nasty-planes-jam.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@qwik.dev/core': minor
3+
---
4+
5+
FEAT: `useSerialized$(fn)` and `createSerialized$(fn)` allow serializing custom objects. You must provide a
6+
function that converts the custom object to a serializable one via the `[SerializerSymbol]`
7+
property, and then provide `use|createSerialized$(fn)` with the function that creates the custom object
8+
from the serialized data. This will lazily create the value when needed.

packages/docs/src/routes/api/qwik/api.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,20 @@
221221
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-context.ts",
222222
"mdFile": "core.createcontextid.md"
223223
},
224+
{
225+
"name": "createSerialized$",
226+
"id": "createserialized_",
227+
"hierarchy": [
228+
{
229+
"name": "createSerialized$",
230+
"id": "createserialized_"
231+
}
232+
],
233+
"kind": "Function",
234+
"content": "Create a signal that holds a custom serializable value. See `useSerialized$` for more details.\n\n\n```typescript\ncreateSerialized$: <T extends CustomSerializable<T, S>, S, F extends ConstructorFn<T, S> = ConstructorFn<T, S>>(qrl: F | QRL<F>) => SerializedSignal<T, S, F>\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\nF \\| [QRL](#qrl)<!-- -->&lt;F&gt;\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>\n**Returns:**\n\nSerializedSignal&lt;T, S, F&gt;",
235+
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/signal/signal.public.ts",
236+
"mdFile": "core.createserialized_.md"
237+
},
224238
{
225239
"name": "createSignal",
226240
"id": "createsignal",
@@ -2031,6 +2045,20 @@
20312045
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-resource-dollar.ts",
20322046
"mdFile": "core.useresource_.md"
20332047
},
2048+
{
2049+
"name": "useSerialized$",
2050+
"id": "useserialized_",
2051+
"hierarchy": [
2052+
{
2053+
"name": "useSerialized$",
2054+
"id": "useserialized_"
2055+
}
2056+
],
2057+
"kind": "Variable",
2058+
"content": "Creates a signal which holds a custom serializable value. It requires that the value implements the `CustomSerializable` type, which means having a function under the `[SerializeSymbol]` property that returns a serializable value when called.\n\nThe `fn` you pass is called with the result of the serialization (in the browser, only when the value is needed), or `undefined` when not yet initialized. If you refer to other signals, `fn` will be called when those change just like computed signals, and then the argument will be the previous output, not the serialized result.\n\nThis is useful when using third party libraries that use custom objects that are not serializable.\n\nNote that the `fn` is called lazily, so it won't impact container resume.\n\n\n```typescript\nuseSerialized$: {\n fn: <T extends CustomSerializable<T, S>, S, F extends ConstructorFn<T, S> = ConstructorFn<T, S>>(fn: F | QRL<F>) => T extends Promise<any> ? never : ReadonlySignal<T>;\n}['fn']\n```\n\n\n\n```tsx\nclass MyCustomSerializable {\n constructor(public n: number) {}\n inc() {\n this.n++;\n }\n [SerializeSymbol]() {\n return this.n;\n }\n}\nconst Cmp = component$(() => {\n const custom = useSerialized$<MyCustomSerializable, number>(\n (prev) =>\n new MyCustomSerializable(prev instanceof MyCustomSerializable ? prev : (prev ?? 3))\n );\n return <div onClick$={() => custom.value.inc()}>{custom.value.n}</div>;\n});\n```",
2059+
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-serializer.ts",
2060+
"mdFile": "core.useserialized_.md"
2061+
},
20342062
{
20352063
"name": "useServerData",
20362064
"id": "useserverdata",

packages/docs/src/routes/api/qwik/index.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,51 @@ The name of the context.
723723

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

726+
## createSerialized$
727+
728+
Create a signal that holds a custom serializable value. See `useSerialized$` for more details.
729+
730+
```typescript
731+
createSerialized$: <
732+
T extends CustomSerializable<T, S>,
733+
S,
734+
F extends ConstructorFn<T, S> = ConstructorFn<T, S>,
735+
>(
736+
qrl: F | QRL<F>,
737+
) => SerializedSignal<T, S, F>;
738+
```
739+
740+
<table><thead><tr><th>
741+
742+
Parameter
743+
744+
</th><th>
745+
746+
Type
747+
748+
</th><th>
749+
750+
Description
751+
752+
</th></tr></thead>
753+
<tbody><tr><td>
754+
755+
qrl
756+
757+
</td><td>
758+
759+
F \| [QRL](#qrl)&lt;F&gt;
760+
761+
</td><td>
762+
763+
</td></tr>
764+
</tbody></table>
765+
**Returns:**
766+
767+
SerializedSignal&lt;T, S, F&gt;
768+
769+
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/signal/signal.public.ts)
770+
726771
## createSignal
727772

728773
Creates a Signal with the given value. If no value is given, the signal is created with `undefined`.
@@ -4905,6 +4950,45 @@ _(Optional)_
49054950
49064951
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-resource-dollar.ts)
49074952
4953+
## useSerialized$
4954+
4955+
Creates a signal which holds a custom serializable value. It requires that the value implements the `CustomSerializable` type, which means having a function under the `[SerializeSymbol]` property that returns a serializable value when called.
4956+
4957+
The `fn` you pass is called with the result of the serialization (in the browser, only when the value is needed), or `undefined` when not yet initialized. If you refer to other signals, `fn` will be called when those change just like computed signals, and then the argument will be the previous output, not the serialized result.
4958+
4959+
This is useful when using third party libraries that use custom objects that are not serializable.
4960+
4961+
Note that the `fn` is called lazily, so it won't impact container resume.
4962+
4963+
```typescript
4964+
useSerialized$: {
4965+
fn: <T extends CustomSerializable<T, S>, S, F extends ConstructorFn<T, S> = ConstructorFn<T, S>>(fn: F | QRL<F>) => T extends Promise<any> ? never : ReadonlySignal<T>;
4966+
}['fn']
4967+
```
4968+
4969+
```tsx
4970+
class MyCustomSerializable {
4971+
constructor(public n: number) {}
4972+
inc() {
4973+
this.n++;
4974+
}
4975+
[SerializeSymbol]() {
4976+
return this.n;
4977+
}
4978+
}
4979+
const Cmp = component$(() => {
4980+
const custom = useSerialized$<MyCustomSerializable, number>(
4981+
(prev) =>
4982+
new MyCustomSerializable(
4983+
prev instanceof MyCustomSerializable ? prev : (prev ?? 3),
4984+
),
4985+
);
4986+
return <div onClick$={() => custom.value.inc()}>{custom.value.n}</div>;
4987+
});
4988+
```
4989+
4990+
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-serializer.ts)
4991+
49084992
## useServerData
49094993
49104994
```typescript

packages/qwik/public.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export {
66
ComputedSignal,
77
ContextId,
88
createComputed$,
9+
createSerialized$,
910
createContextId,
1011
createSignal,
1112
CSSProperties,
@@ -60,6 +61,7 @@ export {
6061
useOnDocument,
6162
useOnWindow,
6263
useResource$,
64+
useSerialized$,
6365
useServerData,
6466
useSignal,
6567
useStore,

packages/qwik/src/core/api.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,18 @@ export const createComputedQrl: <T>(qrl: QRL<() => T>) => T extends Promise<any>
119119
// @public
120120
export const createContextId: <STATE = unknown>(name: string) => ContextId<STATE>;
121121

122+
// Warning: (ae-forgotten-export) The symbol "CustomSerializable" needs to be exported by the entry point index.d.ts
123+
// Warning: (ae-forgotten-export) The symbol "ConstructorFn" needs to be exported by the entry point index.d.ts
124+
// Warning: (ae-forgotten-export) The symbol "SerializedSignal" needs to be exported by the entry point index.d.ts
125+
//
126+
// @public
127+
export const createSerialized$: <T extends CustomSerializable<T, S>, S, F extends ConstructorFn<T, S> = ConstructorFn<T, S>>(qrl: F | QRL<F>) => SerializedSignal<T, S, F>;
128+
129+
// Warning: (ae-internal-missing-underscore) The name "createSerializedQrl" should be prefixed with an underscore because the declaration is marked as @internal
130+
//
131+
// @internal (undocumented)
132+
export const createSerializedQrl: <T extends CustomSerializable<T, S>, S, F extends ConstructorFn<T, S> = ConstructorFn<T, S>>(qrl: QRL<F>) => SerializedSignal<T, S, F>;
133+
122134
// @public
123135
export const createSignal: {
124136
<T>(): Signal<T | undefined>;
@@ -1092,6 +1104,16 @@ export const useResource$: <T>(generatorFn: ResourceFn<T>, opts?: ResourceOption
10921104
// @internal (undocumented)
10931105
export const useResourceQrl: <T>(qrl: QRL<ResourceFn<T>>, opts?: ResourceOptions) => ResourceReturn<T>;
10941106

1107+
// @public
1108+
export const useSerialized$: {
1109+
fn: <T extends CustomSerializable<T, S>, S, F extends ConstructorFn<T, S> = ConstructorFn<T, S>>(fn: F | QRL<F>) => T extends Promise<any> ? never : ReadonlySignal<T>;
1110+
}['fn'];
1111+
1112+
// Warning: (ae-internal-missing-underscore) The name "useSerializedQrl" should be prefixed with an underscore because the declaration is marked as @internal
1113+
//
1114+
// @internal (undocumented)
1115+
export const useSerializedQrl: <T extends CustomSerializable<T, S>, S, F extends ConstructorFn<T, S>>(qrl: QRL<F>) => T extends Promise<any> ? never : ReadonlySignal<T>;
1116+
10951117
// @public (undocumented)
10961118
export function useServerData<T>(key: string): T | undefined;
10971119

packages/qwik/src/core/index.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ export type { ContextId } from './use/use-context';
110110
export type { UseStoreOptions } from './use/use-store.public';
111111
export type { ComputedFn } from './use/use-computed';
112112
export { useComputedQrl } from './use/use-computed';
113+
export { useSerializedQrl, useSerialized$ } from './use/use-serialized';
113114
export type { OnVisibleTaskOptions, VisibleTaskStrategy } from './use/use-visible-task';
114115
export { useVisibleTaskQrl } from './use/use-visible-task';
115116
export type { EagernessOptions, TaskCtx, TaskFn, Tracker, UseTaskOptions } from './use/use-task';
@@ -132,7 +133,14 @@ export { useComputed$ } from './use/use-computed-dollar';
132133
export { useErrorBoundary } from './use/use-error-boundary';
133134
export type { ErrorBoundaryStore } from './shared/error/error-handling';
134135
export { type ReadonlySignal, type Signal, type ComputedSignal } from './signal/signal.public';
135-
export { isSignal, createSignal, createComputedQrl, createComputed$ } from './signal/signal.public';
136+
export {
137+
isSignal,
138+
createSignal,
139+
createComputedQrl,
140+
createComputed$,
141+
createSerializedQrl,
142+
createSerialized$,
143+
} from './signal/signal.public';
136144
export { EffectPropData as _EffectData } from './signal/signal';
137145

138146
//////////////////////////////////////////////////////////////////////////////////////////

packages/qwik/src/core/shared/shared-serialization.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,15 @@ import { type DomContainer } from '../client/dom-container';
88
import type { VNode } from '../client/types';
99
import { vnode_getNode, vnode_isVNode, vnode_locate, vnode_toString } from '../client/vnode';
1010
import { NEEDS_COMPUTATION } from '../signal/flags';
11-
import { ComputedSignal, EffectPropData, Signal, WrappedSignal } from '../signal/signal';
11+
import {
12+
ComputedSignal,
13+
EffectPropData,
14+
SerializedSignal,
15+
Signal,
16+
WrappedSignal,
17+
isSerializerObj,
18+
type EffectSubscriptions,
19+
} from '../signal/signal';
1220
import type { Subscriber } from '../signal/signal-subscriber';
1321
import {
1422
STORE_ARRAY_PROP,
@@ -283,13 +291,16 @@ const inflate = (
283291
signal.$effects$ = d.slice(5);
284292
break;
285293
}
294+
case TypeIds.SerializedSignal:
286295
case TypeIds.ComputedSignal: {
287296
const computed = target as ComputedSignal<unknown>;
288-
const d = data as [QRLInternal<() => {}>, any, unknown?];
297+
const d = data as [QRLInternal<() => {}>, EffectSubscriptions[] | null, unknown?];
289298
computed.$computeQrl$ = d[0];
290299
computed.$effects$ = d[1];
291-
if (d.length === 3) {
300+
if (d.length >= 3) {
292301
computed.$untrackedValue$ = d[2];
302+
// The serialized signal is always invalid so it can recreate the custom object
303+
computed.$invalid$ = typeId === TypeIds.SerializedSignal;
293304
} else {
294305
computed.$invalid$ = true;
295306
/**
@@ -478,6 +489,8 @@ const allocate = (container: DeserializeContainer, typeId: number, value: unknow
478489
return new WrappedSignal(container as any, null!, null!, null!);
479490
case TypeIds.ComputedSignal:
480491
return new ComputedSignal(container as any, null!);
492+
case TypeIds.SerializedSignal:
493+
return new SerializedSignal(container as any, null!);
481494
case TypeIds.Store:
482495
case TypeIds.StoreArray:
483496
// ignore allocate, we need to assign target while creating store
@@ -884,7 +897,7 @@ export const createSerializationContext = (
884897
discoveredValues.push(obj.data);
885898
} else if (Array.isArray(obj)) {
886899
discoveredValues.push(...obj);
887-
} else if (SerializerSymbol in obj && typeof obj[SerializerSymbol] === 'function') {
900+
} else if (isSerializerObj(obj)) {
888901
const result = obj[SerializerSymbol](obj);
889902
serializationResults.set(obj, result);
890903
discoveredValues.push(result);
@@ -1168,7 +1181,7 @@ function serialize(serializationContext: SerializationContext): void {
11681181
* Special case: when a Signal value is an SSRNode, it always needs to be a DOM ref instead.
11691182
* It can never be meant to become a vNode, because vNodes are internal only.
11701183
*/
1171-
const v =
1184+
const v: unknown =
11721185
value instanceof ComputedSignal &&
11731186
(value.$invalid$ || fastSkipSerialize(value.$untrackedValue$))
11741187
? NEEDS_COMPUTATION
@@ -1183,15 +1196,18 @@ function serialize(serializationContext: SerializationContext): void {
11831196
...(value.$effects$ || []),
11841197
]);
11851198
} else if (value instanceof ComputedSignal) {
1186-
const out = [
1199+
const out: [QRLInternal, EffectSubscriptions[] | null, unknown?] = [
11871200
value.$computeQrl$,
11881201
// TODO check if we can use domVRef for effects
11891202
value.$effects$,
11901203
];
11911204
if (v !== NEEDS_COMPUTATION) {
11921205
out.push(v);
11931206
}
1194-
output(TypeIds.ComputedSignal, out);
1207+
output(
1208+
value instanceof SerializedSignal ? TypeIds.SerializedSignal : TypeIds.ComputedSignal,
1209+
out
1210+
);
11951211
} else {
11961212
output(TypeIds.Signal, [v, ...(value.$effects$ || [])]);
11971213
}
@@ -1629,6 +1645,7 @@ export const enum TypeIds {
16291645
Signal,
16301646
WrappedSignal,
16311647
ComputedSignal,
1648+
SerializedSignal,
16321649
Store,
16331650
StoreArray,
16341651
FormData,
@@ -1662,6 +1679,7 @@ export const _typeIdNames = [
16621679
'Signal',
16631680
'WrappedSignal',
16641681
'ComputedSignal',
1682+
'SerializedSignal',
16651683
'Store',
16661684
'StoreArray',
16671685
'FormData',

0 commit comments

Comments
 (0)