Skip to content

Commit ed086ec

Browse files
authored
Update/v2/utils/wrap setter (#884)
2 parents 2a6c5eb + b1442c5 commit ed086ec

5 files changed

Lines changed: 156 additions & 2 deletions

File tree

.changeset/eleven-baths-build.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@solid-primitives/utils": minor
3+
---
4+
5+
new wrapSetter primitive to wrap the setters of signals and stores

packages/utils/README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,41 @@ const { data } = createSSE<Event>(url, { transform: safe(json) });
115115
- **`safe(transform, fallback?)`** - Wrap any transform in a `try/catch`; returns `fallback` instead of throwing
116116
- **`pipe(a, b)`** - Compose two transforms into one
117117

118+
## wrapSetter
119+
120+
It is a typical use case to react on setting a new value; this is especially cumbersome for stores, where you otherwise need the `deep` package to make effects subscribe to all changes. A more performant and simple approach is to wrap the setter of your signal or store. To simplify this approach, we provide a `wrapSetter` function:
121+
122+
```ts
123+
import { createStore } from "solid-js";
124+
import { wrapSetter } from "@solid-primitives/utils";
125+
126+
const [state, setState] = wrapSetter(
127+
createStore(
128+
localStorage.getItem('persistedState')
129+
? JSON.parse(localStorage.getItem('persistedState'))
130+
: initialState
131+
),
132+
(setter) => (next) => {
133+
const output = setState(next);
134+
localStorage.setItem('persistedState', latest(() => JSON.stringify(state)));
135+
return output;
136+
}
137+
);
138+
```
139+
140+
If the signal or store is destructured into a tuple and augmented with additional values, those are left intact in the output. For the TS types to work, you need to `as const` the new tuple:
141+
142+
```ts
143+
import { createSignal } from "solid-js";
144+
import { wrapSetter } from "@solid-primitives/utils";
145+
146+
const augmentedSignal = [...createSignal(0), { extra: "data" }] as const;
147+
const [count, setCount, data] = wrapSetter(
148+
augmented,
149+
(setter) => (next) => (console.log(next), setter(next))
150+
);
151+
```
152+
118153
## Changelog
119154

120155
See [CHANGELOG.md](./CHANGELOG.md)

packages/utils/package.json

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,55 @@
6060
"solid",
6161
"primitives"
6262
],
63+
"primitive": {
64+
"name": "utils",
65+
"stage": 2,
66+
"list": [
67+
"shallowArrayCopy",
68+
"shallowObjectCopy",
69+
"shallowCopy",
70+
"withArrayCopy",
71+
"withObjectCopy",
72+
"withCopy",
73+
"push",
74+
"drop",
75+
"dropRight",
76+
"filterOut",
77+
"filter",
78+
"sort",
79+
"sortBy",
80+
"map",
81+
"slice",
82+
"splice",
83+
"fill",
84+
"concat",
85+
"remove",
86+
"removeItems",
87+
"flatten",
88+
"filterInstance",
89+
"filterOutInstance",
90+
"omit",
91+
"pick",
92+
"split",
93+
"merge",
94+
"get",
95+
"update",
96+
"add",
97+
"substract",
98+
"multiply",
99+
"divide",
100+
"power",
101+
"clamp",
102+
"json",
103+
"ndjson",
104+
"lines",
105+
"number",
106+
"safe",
107+
"pipe",
108+
"wrapSetter"
109+
],
110+
"category": "Utilities"
111+
},
63112
"peerDependencies": {
64113
"@solidjs/web": "^2.0.0-beta.10",
65114
"solid-js": "^2.0.0-beta.10"

packages/utils/src/index.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@ import {
22
getOwner,
33
onCleanup,
44
createSignal,
5+
createStore,
56
type Accessor,
67
untrack,
78
type EffectFunction,
89
type NoInfer,
10+
type Setter,
911
type SignalOptions,
12+
type Signal,
13+
type Store,
14+
type StoreSetter,
1015
sharedConfig,
1116
onSettled,
1217
DEV,
@@ -396,3 +401,31 @@ export function safe<T>(
396401
export function pipe<A, B>(a: (raw: string) => A, b: (a: A) => B): (raw: string) => B {
397402
return (raw: string): B => b(a(raw));
398403
}
404+
405+
/**
406+
* Wraps a setter function of any signal or store
407+
*
408+
* ```ts
409+
* const [data, setData] = wrapSetter(
410+
* createSignal(initialData),
411+
* (setter) => (next) => { console.log(next); return setter(next); },
412+
* );
413+
* ```
414+
* If you destructure signal or store in a longer tuple, you need to use a const assertion for the types to work.
415+
*/
416+
export function wrapSetter<T>(signal: Signal<T>, wrapper: (setter: Setter<T>) => Setter<T>): Signal<T>;
417+
export function wrapSetter<T>(store: [Store<T>, StoreSetter<T>], wrapper: (setter: StoreSetter<T>) => StoreSetter<T>): [Store<T>, StoreSetter<T>];
418+
export function wrapSetter<T, S extends Signal<T> | [Store<T>, StoreSetter<T>] | [...Signal<T>, ...any[]] | [Store<T>, StoreSetter<T>, ...any[]]>(
419+
signalOrStore: S,
420+
wrapper: (setter: S[1]) => S[1]
421+
): S;
422+
export function wrapSetter<T, S extends Signal<T> | [Store<T>, StoreSetter<T>] | readonly [...Signal<T>, ...any[]] | readonly [Store<T>, StoreSetter<T>, ...any[]]>(
423+
signalOrStore: S,
424+
wrapper: (setter: S[1]) => S[1]
425+
): S;
426+
export function wrapSetter<T, S extends Signal<T> | [Store<T>, StoreSetter<T>] | [...Signal<T>, ...any[]] | [Store<T>, StoreSetter<T>, ...any[]]>(
427+
signalOrStore: S,
428+
wrapper: (setter: S[1]) => S[1]
429+
): S {
430+
return [signalOrStore[0], wrapper(signalOrStore[1]), ...signalOrStore.slice(2)] as S;
431+
}

packages/utils/test/index.test.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { describe, test, expect, assert } from "vitest";
2-
import { handleDiffArray, arrayEquals, createHydratableSignal } from "../src/index.js";
1+
import { describe, test, expect, assert, vi } from "vitest";
2+
import { createSignal, createStore, flush, type Signal } from "solid-js";
3+
import { handleDiffArray, arrayEquals, createHydratableSignal, wrapSetter } from "../src/index.js";
34

45
describe("handleDiffArray", () => {
56
test("handleAdded called for new array", () => {
@@ -102,3 +103,34 @@ describe("createHydratableSignal", () => {
102103
expect(setState).toBeInstanceOf(Function);
103104
});
104105
});
106+
107+
describe("wrapSetter", () => {
108+
test("wraps a signal", () => {
109+
const wrapped = vi.fn((x) => x);
110+
const [state, setState] = wrapSetter(createSignal(0), (setter) => (next) => wrapped(setter(next)));
111+
setState(1);
112+
flush();
113+
expect(state()).toBe(1);
114+
expect(wrapped).toHaveBeenCalledWith(1);
115+
setState(c => c + 1);
116+
flush();
117+
expect(state()).toBe(2);
118+
});
119+
test("wraps a store", () => {
120+
const wrapped = vi.fn((x) => x);
121+
const [state, setState] = wrapSetter(createStore({ on: false }), (setter) => (next) => wrapped(setter(next)));
122+
setState((s) => { s.on = !s.on; });
123+
flush();
124+
expect(state.on).toBe(true);
125+
expect(wrapped).toHaveBeenCalled();
126+
});
127+
test("leaves additional values in the new tuple", () => {
128+
const wrapped = vi.fn((x) => x);
129+
const modifiedSignal = [...createSignal(0), {} as Record<string, number>, [] as string[]] as const;
130+
const wrappedSignal = wrapSetter(modifiedSignal, (setter) => (next) => wrapped(setter(next)));
131+
expect(wrappedSignal[0]).toBe(modifiedSignal[0]);
132+
expect(wrappedSignal[2]).toBe(modifiedSignal[2]);
133+
expect(wrappedSignal[3]).toBe(modifiedSignal[3]);
134+
expect(wrappedSignal).toHaveLength(modifiedSignal.length);
135+
});
136+
});

0 commit comments

Comments
 (0)