Skip to content

Commit 18169a7

Browse files
committed
feat(core): use/createSerialized$
1 parent 0a5e84d commit 18169a7

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,
@@ -280,13 +288,16 @@ const inflate = (container: DeserializeContainer, target: any, typeId: TypeIds,
280288
signal.$effects$ = d.slice(5);
281289
break;
282290
}
291+
case TypeIds.SerializedSignal:
283292
case TypeIds.ComputedSignal: {
284293
const computed = target as ComputedSignal<unknown>;
285-
const d = data as [QRLInternal<() => {}>, any, unknown?];
294+
const d = data as [QRLInternal<() => {}>, EffectSubscriptions[] | null, unknown?];
286295
computed.$computeQrl$ = d[0];
287296
computed.$effects$ = d[1];
288-
if (d.length === 3) {
297+
if (d.length >= 3) {
289298
computed.$untrackedValue$ = d[2];
299+
// The serialized signal is always invalid so it can recreate the custom object
300+
computed.$invalid$ = typeId === TypeIds.SerializedSignal;
290301
} else {
291302
computed.$invalid$ = true;
292303
/**
@@ -474,6 +485,8 @@ const allocate = (container: DeserializeContainer, typeId: number, value: unknow
474485
return new WrappedSignal(container as any, null!, null!, null!);
475486
case TypeIds.ComputedSignal:
476487
return new ComputedSignal(container as any, null!);
488+
case TypeIds.SerializedSignal:
489+
return new SerializedSignal(container as any, null!);
477490
case TypeIds.Store:
478491
return createStore(container as any, {}, 0);
479492
case TypeIds.StoreArray:
@@ -880,7 +893,7 @@ export const createSerializationContext = (
880893
discoveredValues.push(obj.data);
881894
} else if (Array.isArray(obj)) {
882895
discoveredValues.push(...obj);
883-
} else if (SerializerSymbol in obj && typeof obj[SerializerSymbol] === 'function') {
896+
} else if (isSerializerObj(obj)) {
884897
const result = obj[SerializerSymbol](obj);
885898
serializationResults.set(obj, result);
886899
discoveredValues.push(result);
@@ -1164,7 +1177,7 @@ function serialize(serializationContext: SerializationContext): void {
11641177
* Special case: when a Signal value is an SSRNode, it always needs to be a DOM ref instead.
11651178
* It can never be meant to become a vNode, because vNodes are internal only.
11661179
*/
1167-
const v =
1180+
const v: unknown =
11681181
value instanceof ComputedSignal &&
11691182
(value.$invalid$ || fastSkipSerialize(value.$untrackedValue$))
11701183
? NEEDS_COMPUTATION
@@ -1179,15 +1192,18 @@ function serialize(serializationContext: SerializationContext): void {
11791192
...(value.$effects$ || []),
11801193
]);
11811194
} else if (value instanceof ComputedSignal) {
1182-
const out = [
1195+
const out: [QRLInternal, EffectSubscriptions[] | null, unknown?] = [
11831196
value.$computeQrl$,
11841197
// TODO check if we can use domVRef for effects
11851198
value.$effects$,
11861199
];
11871200
if (v !== NEEDS_COMPUTATION) {
11881201
out.push(v);
11891202
}
1190-
output(TypeIds.ComputedSignal, out);
1203+
output(
1204+
value instanceof SerializedSignal ? TypeIds.SerializedSignal : TypeIds.ComputedSignal,
1205+
out
1206+
);
11911207
} else {
11921208
output(TypeIds.Signal, [v, ...(value.$effects$ || [])]);
11931209
}
@@ -1625,6 +1641,7 @@ export const enum TypeIds {
16251641
Signal,
16261642
WrappedSignal,
16271643
ComputedSignal,
1644+
SerializedSignal,
16281645
Store,
16291646
StoreArray,
16301647
FormData,
@@ -1658,6 +1675,7 @@ export const _typeIdNames = [
16581675
'Signal',
16591676
'WrappedSignal',
16601677
'ComputedSignal',
1678+
'SerializedSignal',
16611679
'Store',
16621680
'StoreArray',
16631681
'FormData',

0 commit comments

Comments
 (0)