Skip to content
Open
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) => {
const output = setState();
localStorage.setItem('persistedState', latest(() => JSON.stringify(state)));
return output;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so this returns a new setter?

}
);
```

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 @@
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 @@ -169,11 +174,11 @@
const isArray = Array.isArray(deps);
let prevInput: S;
let shouldDefer = true;
return prevValue => {

Check failure on line 177 in packages/utils/src/index.ts

View workflow job for this annotation

GitHub Actions / build-test

Type '(prevValue: NoInfer<Next> | undefined) => Next | undefined' is not assignable to type 'EffectFunction<NoInfer<Next> | undefined>'.
let input: S;
if (isArray) {
input = Array(deps.length) as S;
for (let i = 0; i < deps.length; i++) (input as any[])[i] = deps[i]();

Check failure on line 181 in packages/utils/src/index.ts

View workflow job for this annotation

GitHub Actions / build-test

Cannot invoke an object which is possibly 'undefined'.
} else input = deps();
if (shouldDefer) {
shouldDefer = false;
Expand Down Expand Up @@ -253,14 +258,14 @@
options?: SignalOptions<T>,
): ReturnType<typeof createSignal<T>> {
if (isServer) {
return createSignal(serverValue, options);

Check failure on line 261 in packages/utils/src/index.ts

View workflow job for this annotation

GitHub Actions / build-test

No overload matches this call.
}
if (sharedConfig.hydrating) {
const [state, setState] = createSignal(serverValue, options);

Check failure on line 264 in packages/utils/src/index.ts

View workflow job for this annotation

GitHub Actions / build-test

No overload matches this call.
onSettled(() => setState(() => update()));

Check failure on line 265 in packages/utils/src/index.ts

View workflow job for this annotation

GitHub Actions / build-test

Type 'T' is not assignable to type 'void | (() => void)'.
return [state, setState];
}
return createSignal(update(), options);

Check failure on line 268 in packages/utils/src/index.ts

View workflow job for this annotation

GitHub Actions / build-test

No overload matches this call.
}

/** @deprecated use {@link createHydratableSignal} instead */
Expand Down Expand Up @@ -396,3 +401,31 @@
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);
});
});
Loading