Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/eleven-baths-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solid-primitives/utils": minor
---

new wrapSetter primitive to wrap the setters of signals and stores
35 changes: 35 additions & 0 deletions packages/utils/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,41 @@ const { data } = createSSE<Event>(url, { transform: safe(json) });
- **`safe(transform, fallback?)`** - Wrap any transform in a `try/catch`; returns `fallback` instead of throwing
- **`pipe(a, b)`** - Compose two transforms into one

## wrapSetter

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:

```ts
import { createStore } from "solid-js";
import { wrapSetter } from "@solid-primitives/utils";

const [state, setState] = wrapSetter(
createStore(
localStorage.getItem('persistedState')
? JSON.parse(localStorage.getItem('persistedState'))
: initialState
),
(setter) => (next) => {
const output = setState(next);
localStorage.setItem('persistedState', latest(() => JSON.stringify(state)));
return output;
Comment thread
davedbase marked this conversation as resolved.
}
);
```

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:

```ts
import { createSignal } from "solid-js";
import { wrapSetter } from "@solid-primitives/utils";

const augmentedSignal = [...createSignal(0), { extra: "data" }] as const;
const [count, setCount, data] = wrapSetter(
augmented,
(setter) => (next) => (console.log(next), setter(next))
);
```

## Changelog

See [CHANGELOG.md](./CHANGELOG.md)
49 changes: 49 additions & 0 deletions packages/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,55 @@
"solid",
"primitives"
],
"primitive": {
"name": "utils",
"stage": 2,
"list": [
"shallowArrayCopy",
"shallowObjectCopy",
"shallowCopy",
"withArrayCopy",
"withObjectCopy",
"withCopy",
"push",
"drop",
"dropRight",
"filterOut",
"filter",
"sort",
"sortBy",
"map",
"slice",
"splice",
"fill",
"concat",
"remove",
"removeItems",
"flatten",
"filterInstance",
"filterOutInstance",
"omit",
"pick",
"split",
"merge",
"get",
"update",
"add",
"substract",
"multiply",
"divide",
"power",
"clamp",
"json",
"ndjson",
"lines",
"number",
"safe",
"pipe",
"wrapSetter"
],
"category": "Utilities"
},
"peerDependencies": {
"@solidjs/web": "^2.0.0-beta.10",
"solid-js": "^2.0.0-beta.10"
Expand Down
33 changes: 33 additions & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@ import {
getOwner,
onCleanup,
createSignal,
createStore,
type Accessor,
untrack,
type EffectFunction,
type NoInfer,
type Setter,
type SignalOptions,
type Signal,
type Store,
type StoreSetter,
sharedConfig,
onSettled,
DEV,
Expand Down Expand Up @@ -396,3 +401,31 @@ export function safe<T>(
export function pipe<A, B>(a: (raw: string) => A, b: (a: A) => B): (raw: string) => B {
return (raw: string): B => b(a(raw));
}

/**
* Wraps a setter function of any signal or store
*
* ```ts
* const [data, setData] = wrapSetter(
* createSignal(initialData),
* (setter) => (next) => { console.log(next); return setter(next); },
* );
* ```
* If you destructure signal or store in a longer tuple, you need to use a const assertion for the types to work.
*/
export function wrapSetter<T>(signal: Signal<T>, wrapper: (setter: Setter<T>) => Setter<T>): Signal<T>;
export function wrapSetter<T>(store: [Store<T>, StoreSetter<T>], wrapper: (setter: StoreSetter<T>) => StoreSetter<T>): [Store<T>, StoreSetter<T>];
export function wrapSetter<T, S extends Signal<T> | [Store<T>, StoreSetter<T>] | [...Signal<T>, ...any[]] | [Store<T>, StoreSetter<T>, ...any[]]>(
signalOrStore: S,
wrapper: (setter: S[1]) => S[1]
): S;
export function wrapSetter<T, S extends Signal<T> | [Store<T>, StoreSetter<T>] | readonly [...Signal<T>, ...any[]] | readonly [Store<T>, StoreSetter<T>, ...any[]]>(
signalOrStore: S,
wrapper: (setter: S[1]) => S[1]
): S;
export function wrapSetter<T, S extends Signal<T> | [Store<T>, StoreSetter<T>] | [...Signal<T>, ...any[]] | [Store<T>, StoreSetter<T>, ...any[]]>(
signalOrStore: S,
wrapper: (setter: S[1]) => S[1]
): S {
return [signalOrStore[0], wrapper(signalOrStore[1]), ...signalOrStore.slice(2)] as S;
}
36 changes: 34 additions & 2 deletions packages/utils/test/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, test, expect, assert } from "vitest";
import { handleDiffArray, arrayEquals, createHydratableSignal } from "../src/index.js";
import { describe, test, expect, assert, vi } from "vitest";
import { createSignal, createStore, flush, type Signal } from "solid-js";
import { handleDiffArray, arrayEquals, createHydratableSignal, wrapSetter } from "../src/index.js";

describe("handleDiffArray", () => {
test("handleAdded called for new array", () => {
Expand Down Expand Up @@ -102,3 +103,34 @@ describe("createHydratableSignal", () => {
expect(setState).toBeInstanceOf(Function);
});
});

describe("wrapSetter", () => {
test("wraps a signal", () => {
const wrapped = vi.fn((x) => x);
const [state, setState] = wrapSetter(createSignal(0), (setter) => (next) => wrapped(setter(next)));
setState(1);
flush();
expect(state()).toBe(1);
expect(wrapped).toHaveBeenCalledWith(1);
setState(c => c + 1);
flush();
expect(state()).toBe(2);
});
test("wraps a store", () => {
const wrapped = vi.fn((x) => x);
const [state, setState] = wrapSetter(createStore({ on: false }), (setter) => (next) => wrapped(setter(next)));
setState((s) => { s.on = !s.on; });
flush();
expect(state.on).toBe(true);
expect(wrapped).toHaveBeenCalled();
});
test("leaves additional values in the new tuple", () => {
const wrapped = vi.fn((x) => x);
const modifiedSignal = [...createSignal(0), {} as Record<string, number>, [] as string[]] as const;
const wrappedSignal = wrapSetter(modifiedSignal, (setter) => (next) => wrapped(setter(next)));
expect(wrappedSignal[0]).toBe(modifiedSignal[0]);
expect(wrappedSignal[2]).toBe(modifiedSignal[2]);
expect(wrappedSignal[3]).toBe(modifiedSignal[3]);
expect(wrappedSignal).toHaveLength(modifiedSignal.length);
});
});