Skip to content

Commit 4d056d4

Browse files
wmertensVarixomhevery
committed
feat(useSerializer$): custom serialization
Co-authored-by: Varixo <[email protected]> Co-authored-by: Miško Hevery <[email protected]>
1 parent ba1bf52 commit 4d056d4

File tree

24 files changed

+1327
-128
lines changed

24 files changed

+1327
-128
lines changed

.changeset/tricky-peaches-buy.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@qwik.dev/core': minor
3+
---
4+
5+
FEAT: `useSerializer$`, `createSerializer$`: Create a Signal holding a custom serializable value. See {@link useSerializer$} for more details.
6+
7+
FEAT: `NoSerializeSymbol`: objects that have this symbol will not be serialized.
8+
9+
FEAT: `SerializerSymbol`: When defined on an object, this function will get called with the object and is expected to returned a serializable object literal representing this object. Use this to remove data cached data, consolidate things, integrate with other libraries, etc.

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

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@
390390
}
391391
],
392392
"kind": "Function",
393-
"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;",
393+
"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 must 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;",
394394
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/signal/signal.public.ts",
395395
"mdFile": "core.createcomputed_.md"
396396
},
@@ -408,6 +408,20 @@
408408
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-context.ts",
409409
"mdFile": "core.createcontextid.md"
410410
},
411+
{
412+
"name": "createSerializer$",
413+
"id": "createserializer_",
414+
"hierarchy": [
415+
{
416+
"name": "createSerializer$",
417+
"id": "createserializer_"
418+
}
419+
],
420+
"kind": "Function",
421+
"content": "Create a signal that holds a custom serializable value. See [useSerializer$](#useserializer_) for more details.\n\n\n```typescript\ncreateSerializer$: <T, S>(arg: SerializerArg<T, S>) => T extends Promise<any> ? never : SerializerSignal<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\narg\n\n\n</td><td>\n\nSerializerArg&lt;T, S&gt;\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>\n**Returns:**\n\nT extends Promise&lt;any&gt; ? never : SerializerSignal&lt;T&gt;",
422+
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/signal/signal.public.ts",
423+
"mdFile": "core.createserializer_.md"
424+
},
411425
{
412426
"name": "createSignal",
413427
"id": "createsignal",
@@ -1014,6 +1028,20 @@
10141028
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/utils/serialize-utils.ts",
10151029
"mdFile": "core.noserialize.md"
10161030
},
1031+
{
1032+
"name": "NoSerializeSymbol",
1033+
"id": "noserializesymbol",
1034+
"hierarchy": [
1035+
{
1036+
"name": "NoSerializeSymbol",
1037+
"id": "noserializesymbol"
1038+
}
1039+
],
1040+
"kind": "Variable",
1041+
"content": "If an object has this property, it will not be serialized. Use this on prototypes to avoid having to call `noSerialize()` on every object.\n\n\n```typescript\nNoSerializeSymbol: unique symbol\n```",
1042+
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/utils/serialize-utils.ts",
1043+
"mdFile": "core.noserializesymbol.md"
1044+
},
10171045
{
10181046
"name": "OnRenderFn",
10191047
"id": "onrenderfn",
@@ -1714,6 +1742,20 @@
17141742
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-resource.ts",
17151743
"mdFile": "core.resourcereturn.md"
17161744
},
1745+
{
1746+
"name": "SerializerSymbol",
1747+
"id": "serializersymbol",
1748+
"hierarchy": [
1749+
{
1750+
"name": "SerializerSymbol",
1751+
"id": "serializersymbol"
1752+
}
1753+
],
1754+
"kind": "Variable",
1755+
"content": "If an object has this property as a function, it will be called with the object and should return a serializable value.\n\nThis can be used to clean up, integrate with other libraries, etc.\n\nThe type your object should conform to is:\n\n```ts\n{\n [SerializerSymbol]: (this: YourType, toSerialize: YourType) => YourSerializableType;\n}\n```\n\n\n```typescript\nSerializerSymbol: unique symbol\n```",
1756+
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/utils/serialize-utils.ts",
1757+
"mdFile": "core.serializersymbol.md"
1758+
},
17171759
{
17181760
"name": "setPlatform",
17191761
"id": "setplatform",
@@ -2060,8 +2102,8 @@
20602102
}
20612103
],
20622104
"kind": "Function",
2063-
"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-computed\").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-computed\").[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;",
2064-
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-computed-dollar.ts",
2105+
"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: 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[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 : [ReadonlySignal](#readonlysignal)<!-- -->&lt;T&gt;",
2106+
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-computed.ts",
20652107
"mdFile": "core.usecomputed_.md"
20662108
},
20672109
{
@@ -2190,6 +2232,20 @@
21902232
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-resource-dollar.ts",
21912233
"mdFile": "core.useresource_.md"
21922234
},
2235+
{
2236+
"name": "useSerializer$",
2237+
"id": "useserializer_",
2238+
"hierarchy": [
2239+
{
2240+
"name": "useSerializer$",
2241+
"id": "useserializer_"
2242+
}
2243+
],
2244+
"kind": "Variable",
2245+
"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\nuseSerializer$: typeof createSerializer$\n```\n\n\n\n```tsx\nclass MyCustomSerializable {\n constructor(public n: number) {}\n inc() {\n this.n++;\n }\n}\nconst Cmp = component$(() => {\n const custom = useSerializer$({\n deserialize: (data) => new MyCustomSerializable(data),\n serialize: (data) => data.n,\n initial: 2,\n });\n return <div onClick$={() => custom.value.inc()}>{custom.value.n}</div>;\n});\n```\n\n\nWhen using a Signal as the data to create the object, you need to pass the configuration as a function, and you can then also provide the `update` function to update the object when the signal changes.\n\nBy returning an object from `update`<!-- -->, you signal that the listeners have to be notified. You can mutate the current object but you should return it so that it will trigger listeners.\n\n```tsx\nconst Cmp = component$(() => {\n const n = useSignal(2);\n const custom = useSerializer$(() =>\n ({\n deserialize: () => new MyCustomSerializable(n.value),\n update: (current) => {\n current.n = n.value;\n return current;\n }\n })\n );\n return <div onClick$={() => n.value++}>{custom.value.n}</div>;\n});\n```",
2246+
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-serializer.ts",
2247+
"mdFile": "core.useserializer_.md"
2248+
},
21932249
{
21942250
"name": "useServerData",
21952251
"id": "useserverdata",

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

Lines changed: 125 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -669,7 +669,7 @@ Description
669669
670670
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.
671671
672-
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.
672+
The QRL must be a function which returns the value of the signal. The function must not have side effects, and it must be synchronous.
673673
674674
If you need the function to be async, use `useSignal` and `useTask$` instead.
675675
@@ -789,6 +789,45 @@ The name of the context.
789789

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

792+
## createSerializer$
793+
794+
Create a signal that holds a custom serializable value. See [useSerializer$](#useserializer_) for more details.
795+
796+
```typescript
797+
createSerializer$: <T, S>(arg: SerializerArg<T, S>) => T extends Promise<any> ? never : SerializerSignal<T>
798+
```
799+
800+
<table><thead><tr><th>
801+
802+
Parameter
803+
804+
</th><th>
805+
806+
Type
807+
808+
</th><th>
809+
810+
Description
811+
812+
</th></tr></thead>
813+
<tbody><tr><td>
814+
815+
arg
816+
817+
</td><td>
818+
819+
SerializerArg&lt;T, S&gt;
820+
821+
</td><td>
822+
823+
</td></tr>
824+
</tbody></table>
825+
**Returns:**
826+
827+
T extends Promise&lt;any&gt; ? never : SerializerSignal&lt;T&gt;
828+
829+
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/signal/signal.public.ts)
830+
792831
## createSignal
793832

794833
Creates a Signal with the given value. If no value is given, the signal is created with `undefined`.
@@ -1913,6 +1952,16 @@ export type NoSerialize<T> =
19131952
19141953
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/utils/serialize-utils.ts)
19151954
1955+
## NoSerializeSymbol
1956+
1957+
If an object has this property, it will not be serialized. Use this on prototypes to avoid having to call `noSerialize()` on every object.
1958+
1959+
```typescript
1960+
NoSerializeSymbol: unique symbol
1961+
```
1962+
1963+
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/utils/serialize-utils.ts)
1964+
19161965
## OnRenderFn
19171966
19181967
```typescript
@@ -3607,6 +3656,26 @@ export type ResourceReturn<T> =
36073656
36083657
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-resource.ts)
36093658
3659+
## SerializerSymbol
3660+
3661+
If an object has this property as a function, it will be called with the object and should return a serializable value.
3662+
3663+
This can be used to clean up, integrate with other libraries, etc.
3664+
3665+
The type your object should conform to is:
3666+
3667+
```ts
3668+
{
3669+
[SerializerSymbol]: (this: YourType, toSerialize: YourType) => YourSerializableType;
3670+
}
3671+
```
3672+
3673+
```typescript
3674+
SerializerSymbol: unique symbol
3675+
```
3676+
3677+
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/utils/serialize-utils.ts)
3678+
36103679
## setPlatform
36113680
36123681
Sets the `CorePlatform`.
@@ -8355,7 +8424,7 @@ Creates a computed signal which is calculated from the given function. A compute
83558424
The function must be synchronous and must not have any side effects.
83568425
83578426
```typescript
8358-
useComputed$: <T>(qrl: import("./use-computed").ComputedFn<T>) => T extends Promise<any> ? never : import("..").ReadonlySignal<T>
8427+
useComputed$: <T>(qrl: ComputedFn<T>) => T extends Promise<any> ? never : ReadonlySignal<T>
83598428
```
83608429
83618430
<table><thead><tr><th>
@@ -8377,17 +8446,17 @@ qrl
83778446
83788447
</td><td>
83798448
8380-
import("./use-computed").[ComputedFn](#computedfn)&lt;T&gt;
8449+
[ComputedFn](#computedfn)&lt;T&gt;
83818450
83828451
</td><td>
83838452
83848453
</td></tr>
83858454
</tbody></table>
83868455
**Returns:**
83878456
8388-
T extends Promise&lt;any&gt; ? never : import("..").[ReadonlySignal](#readonlysignal)&lt;T&gt;
8457+
T extends Promise&lt;any&gt; ? never : [ReadonlySignal](#readonlysignal)&lt;T&gt;
83898458
8390-
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-computed-dollar.ts)
8459+
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-computed.ts)
83918460
83928461
## useConstant
83938462
@@ -8844,6 +8913,57 @@ _(Optional)_
88448913
88458914
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-resource-dollar.ts)
88468915
8916+
## useSerializer$
8917+
8918+
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.
8919+
8920+
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.
8921+
8922+
This is useful when using third party libraries that use custom objects that are not serializable.
8923+
8924+
Note that the `fn` is called lazily, so it won't impact container resume.
8925+
8926+
```typescript
8927+
useSerializer$: typeof createSerializer$;
8928+
```
8929+
8930+
```tsx
8931+
class MyCustomSerializable {
8932+
constructor(public n: number) {}
8933+
inc() {
8934+
this.n++;
8935+
}
8936+
}
8937+
const Cmp = component$(() => {
8938+
const custom = useSerializer$({
8939+
deserialize: (data) => new MyCustomSerializable(data),
8940+
serialize: (data) => data.n,
8941+
initial: 2,
8942+
});
8943+
return <div onClick$={() => custom.value.inc()}>{custom.value.n}</div>;
8944+
});
8945+
```
8946+
8947+
When using a Signal as the data to create the object, you need to pass the configuration as a function, and you can then also provide the `update` function to update the object when the signal changes.
8948+
8949+
By returning an object from `update`, you signal that the listeners have to be notified. You can mutate the current object but you should return it so that it will trigger listeners.
8950+
8951+
```tsx
8952+
const Cmp = component$(() => {
8953+
const n = useSignal(2);
8954+
const custom = useSerializer$(() => ({
8955+
deserialize: () => new MyCustomSerializable(n.value),
8956+
update: (current) => {
8957+
current.n = n.value;
8958+
return current;
8959+
},
8960+
}));
8961+
return <div onClick$={() => n.value++}>{custom.value.n}</div>;
8962+
});
8963+
```
8964+
8965+
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-serializer.ts)
8966+
88478967
## useServerData
88488968
88498969
```typescript

0 commit comments

Comments
 (0)