diff --git a/.changeset/flux-store-solid2-migration.md b/.changeset/flux-store-solid2-migration.md new file mode 100644 index 000000000..df73f5054 --- /dev/null +++ b/.changeset/flux-store-solid2-migration.md @@ -0,0 +1,39 @@ +--- +"@solid-primitives/flux-store": major +--- + +Migrate to Solid.js v2.0 (beta.14) + +## Breaking Changes + +**Peer dependencies**: `solid-js@^2.0.0-beta.14` is now required. + +**Store setter is now draft-first.** The `setState` function received in the `actions` callback no longer accepts path-style arguments. Instead, pass a draft mutator function: + +```ts +// Before (Solid 1.x) +actions: setState => ({ + increment(by = 1) { + setState("value", p => p + by); + }, + reset() { + setState("value", 0); + }, +}) + +// After (Solid 2.0) +actions: setState => ({ + increment(by = 1) { + setState(s => { s.value += by; }); + }, + reset() { + setState(s => { s.value = 0; }); + }, +}) +``` + +**`produce` helper removed.** Solid 2.0 stores use draft-first mutation by default, so `produce` is no longer necessary or available. Replace any `setState(produce(s => ...))` calls with `setState(s => ...)`. + +**`batch` wrapper removed from `createAction`.** All writes in Solid 2.0 are auto-batched, so the explicit `batch()` wrap has been removed from `createAction`. Actions remain `untrack`ed. + +**Import paths updated:** `createStore` and `StoreSetter` (formerly `SetStoreFunction`) are now imported from `solid-js` directly (store types were merged into the main package). diff --git a/packages/flux-store/README.md b/packages/flux-store/README.md index 3e4cc50b1..9063dd422 100644 --- a/packages/flux-store/README.md +++ b/packages/flux-store/README.md @@ -8,10 +8,12 @@ [![version](https://img.shields.io/npm/v/@solid-primitives/flux-store?style=for-the-badge)](https://www.npmjs.com/package/@solid-primitives/flux-store) [![stage](https://img.shields.io/endpoint?style=for-the-badge&url=https%3A%2F%2Fraw.githubusercontent.com%2Fsolidjs-community%2Fsolid-primitives%2Fmain%2Fassets%2Fbadges%2Fstage-0.json)](https://github.com/solidjs-community/solid-primitives#contribution-process) -A library for creating Solid stores with implementing state management through explicit getters for reads and actions for writes. +A library for creating Solid stores that enforce a one-way data flow through explicit getters for reads and actions for writes. - [`createFluxStore`](#createfluxstore) — Creates a store instance with explicit getters and actions. -- [`createFluxStoreFactory`](#createfluxstorefactory) — Create a `FluxStore` encapsulated in a factory function for reusable store implementation. +- [`createFluxStoreFactory`](#createfluxstorefactory) — Creates a `FluxStore` encapsulated in a factory function for reusable store instances. +- [`createActions`](#createactions) — Wraps a record of functions so each runs untracked. +- [`createAction`](#createaction) — Wraps a single function so it runs untracked. ## Installation @@ -25,93 +27,108 @@ pnpm add @solid-primitives/flux-store ## `createFluxStore` -Creates a `FluxStore` instance - a solid store that implements state management through explicit getters for reads and actions for writes. +Creates a `FluxStore` — a Solid store that separates reads (`getters`) from writes (`actions`). ### How to use it `createFluxStore` takes two arguments: -- `initialState` - the initial state of the store. - -- `createMethods` - object containing functions to create getters and/or actions. - - - `getters` - functions that return a value from the store's state. - - `actions` - untracked and batched functions that update the store's state. +- `initialState` — the initial state object. +- `createMethods` — object with optional `getters` and required `actions` factory functions. + - `getters(state)` — return a record of functions that read from the reactive store proxy. + - `actions(setState, state)` — return a record of mutation functions. `setState` uses draft-first mutations: pass a function that mutates the draft object directly. ```ts import { createFluxStore } from "@solid-primitives/flux-store"; -const counterState = createFluxStore( - // initial state - { - value: 5, - }, +const counterStore = createFluxStore( + { value: 5 }, { - // reads getters: state => ({ - count() { - return state.value; - }, + count: () => state.value, + isNegative: () => state.value < 0, }), - // writes actions: setState => ({ increment(by = 1) { - setState("value", p => p + by); + setState(s => { s.value += by; }); }, reset() { - setState("value", 0); + setState(s => { s.value = 0; }); }, }), }, ); -// read -counterState.getters.count(); // => 5 - -// write -counterState.actions.increment(); -counterState.getters.count(); // => 6 +counterStore.getters.count(); // => 5 +counterStore.actions.increment(); +counterStore.getters.count(); // => 6 +counterStore.actions.reset(); +counterStore.getters.count(); // => 0 ``` ## `createFluxStoreFactory` -Creates a [`FluxStore`](#createfluxstore) encapsulated in a factory function for reusable store implementation. +Creates a reusable factory function that produces independent `FluxStore` instances from the same schema, with an optional initial-state override per instance. ### How to use it ```ts -const createToggleState = createFluxStoreFactory( - // initial state +import { createFluxStoreFactory } from "@solid-primitives/flux-store"; + +const createToggleStore = createFluxStoreFactory( + { value: false }, { - value: false, + getters: state => ({ + isOn: () => state.value, + }), + actions: setState => ({ + toggle() { + setState(s => { s.value = !s.value; }); + }, + }), }, - // reads - getters: state => ({ - isOn() { - return state.value; - }, - }), - // writes - actions: setState => ({ - toggle() { - setState("value", p => !p); - }, - }), ); +// Each call creates an isolated store instance +const toggleA = createToggleStore({ value: true }); +const toggleB = createToggleStore(); -// state factory can be reused in different components -const toggleState = createToggleState( - // initial state can be overridden - { value: true }, -); +toggleA.getters.isOn(); // => true +toggleB.getters.isOn(); // => false + +toggleA.actions.toggle(); +toggleA.getters.isOn(); // => false +toggleB.getters.isOn(); // => false (unaffected) +``` + +The factory accepts an optional override as a plain object or a function: + +```ts +const store1 = createToggleStore({ value: true }); +const store2 = createToggleStore(defaults => ({ ...defaults, value: true })); +``` + +## `createActions` + +Wraps each function in a record with `createAction` and returns a new object of the same shape. Useful for applying the untracked wrapper to a batch of functions at once. -// read -toggleState.getters.isOn(); // => true +```ts +import { createActions } from "@solid-primitives/flux-store"; + +const actions = createActions({ + increment: () => setCount(c => c + 1), + reset: () => setCount(0), +}); +``` + +## `createAction` + +Wraps a single function so its body runs inside `untrack` — reactive reads inside will not register dependencies and writes will not throw inside owned scopes. + +```ts +import { createAction } from "@solid-primitives/flux-store"; -// write -toggleState.actions.toggle(); -toggleState.getters.isOn(); // => false +const increment = createAction(() => setCount(c => c + 1)); ``` ## Demo diff --git a/packages/flux-store/package.json b/packages/flux-store/package.json index f63a7e26a..a33604cb0 100644 --- a/packages/flux-store/package.json +++ b/packages/flux-store/package.json @@ -18,7 +18,7 @@ "stage": 0, "list": [ "createFluxStore", - "createFluxFactory", + "createFluxStoreFactory", "createActions", "createAction" ], @@ -57,10 +57,10 @@ "test:ssr": "pnpm run vitest --mode ssr" }, "peerDependencies": { - "solid-js": "^1.6.12" + "solid-js": "^2.0.0-beta.14" }, "typesVersions": {}, "devDependencies": { - "solid-js": "^1.9.7" + "solid-js": "2.0.0-beta.14" } } diff --git a/packages/flux-store/src/index.ts b/packages/flux-store/src/index.ts index e05c5102e..4ea24c327 100644 --- a/packages/flux-store/src/index.ts +++ b/packages/flux-store/src/index.ts @@ -1,6 +1,6 @@ -import { batch, untrack } from "solid-js"; -import type { SetStoreFunction } from "solid-js/store"; -import { createStore } from "solid-js/store"; +import { untrack } from "solid-js"; +import type { Store, StoreSetter } from "solid-js"; +import { createStore } from "solid-js"; /** * Type alias for any function with any number of arguments and any return type. @@ -15,13 +15,13 @@ export type AnyFunctionsRecord = { readonly [K in string]: AnyFunction }; export type Actions = { readonly [K in keyof T]: T[K] }; /** - * Identify function creating an action - function for mutating the state. - * Actions are `batch`ed and `untrack`ed by default - no need to wrap them in `batch` and `untrack`. + * Wraps a function so its body runs untracked — reads inside will not register reactive + * dependencies and writes will not throw inside owned scopes. * @param fn the function to wrap - * @returns function of the same signature as `fn` but wrapped in `batch` and `untrack` + * @returns function of the same signature as `fn` but running untracked */ export function createAction(fn: T): T { - return ((...args) => batch(() => untrack(() => fn(...args)))) as T; + return ((...args) => untrack(() => fn(...args))) as T; } /** @@ -87,9 +87,9 @@ export type FluxFactory< * getters: state => ({ * count: () => state.value, * }), - * actions: (setState, state) => ({ - * increment: () => setState(val => ({ ...val, value: val.value + 1 })), - * reset: () => setState("value", 0), + * actions: setState => ({ + * increment: (by = 1) => setState(s => { s.value += by; }), + * reset: () => setState(s => { s.value = 0; }), * }) * }); * @@ -110,13 +110,13 @@ export function createFluxStore< initialState: TState, createMethods: { getters: (state: TState) => TGetters; - actions: (setState: SetStoreFunction, state: TState) => TActions; + actions: (setState: StoreSetter, state: TState) => TActions; }, ): FluxStore; export function createFluxStore( initialState: TState, createMethods: { - actions: (setState: SetStoreFunction, state: TState) => TActions; + actions: (setState: StoreSetter, state: TState) => TActions; }, ): FluxStore; export function createFluxStore< @@ -127,10 +127,13 @@ export function createFluxStore< initialState: TState, createMethods: { getters?: (state: TState) => TGetters; - actions: (setState: SetStoreFunction, state: TState) => TActions; + actions: (setState: StoreSetter, state: TState) => TActions; }, ): FluxStore { - const [state, setState] = createStore(initialState); + const [state, setState] = createStore(initialState as any) as unknown as [ + Store, + StoreSetter, + ]; return { state, getters: createMethods.getters ? createMethods.getters(state) : {}, @@ -154,9 +157,9 @@ export function createFluxStore< * getters: state => ({ * count: () => state.value, * }), - * actions: (setState, state) => ({ - * increment: () => setState(val => ({ ...val, value: val.value + 1 })), - * reset: () => setState("value", 0), + * actions: setState => ({ + * increment: (by = 1) => setState(s => { s.value += by; }), + * reset: () => setState(s => { s.value = 0; }), * }) * }); * @@ -183,13 +186,13 @@ export function createFluxStoreFactory< fallbackState: TState, createMethods: { getters: (state: TState) => TGetters; - actions: (setState: SetStoreFunction, state: TState) => TActions; + actions: (setState: StoreSetter, state: TState) => TActions; }, ): FluxFactory; export function createFluxStoreFactory( fallbackState: TState, createMethods: { - actions: (setState: SetStoreFunction, state: TState) => TActions; + actions: (setState: StoreSetter, state: TState) => TActions; }, ): FluxFactory; export function createFluxStoreFactory< @@ -200,7 +203,7 @@ export function createFluxStoreFactory< fallbackState: TState, createMethods: { getters?: (state: TState) => TGetters; - actions: (setState: SetStoreFunction, state: TState) => TActions; + actions: (setState: StoreSetter, state: TState) => TActions; }, ): FluxFactory { return initialValueOverride => diff --git a/packages/flux-store/test/index.test.tsx b/packages/flux-store/test/index.test.tsx index 431d9657a..33d6588bb 100644 --- a/packages/flux-store/test/index.test.tsx +++ b/packages/flux-store/test/index.test.tsx @@ -1,5 +1,5 @@ import { describe, test, expect } from "vitest"; -import { produce } from "solid-js/store"; +import { flush } from "solid-js"; import { createFluxStoreFactory, createFluxStore } from "../src/index.js"; const id = "test id"; @@ -7,9 +7,9 @@ const initialState = { id, value: true }; const testState = { id, value: true }; const fluxFactory = createFluxStoreFactory(initialState, { - actions: (setState, state) => ({ + actions: setState => ({ set: setState, - toggle: () => setState("value", !state.value), + toggle: () => setState(s => { s.value = !s.value; }), }), getters: state => ({ get: () => state.value, @@ -31,6 +31,7 @@ describe("createFluxFactory", () => { expect(state.value).toBe(testState.value); expect(getters.get()).toBe(testState.value); actions.toggle(); + flush(); expect(state.value).toBe(!testState.value); expect(getters.get()).toBe(!testState.value); }); @@ -51,11 +52,13 @@ describe("createFluxFactory", () => { expect(getters1.get()).toBe(testState.value); expect(getters2.get()).toBe(testState.value); actions2.toggle(); + flush(); expect(state1.value).toBe(testState.value); expect(state2.value).toBe(!testState.value); expect(getters1.get()).toBe(testState.value); expect(getters2.get()).toBe(!testState.value); actions1.toggle(); + flush(); expect(state1.value).toBe(!testState.value); expect(state2.value).toBe(!testState.value); expect(getters1.get()).toBe(!testState.value); @@ -72,11 +75,13 @@ describe("createFluxFactory", () => { expect(getters1.get()).toBe(!testState.value); expect(getters2.get()).toBe(!testState.value); actions2.toggle(); + flush(); expect(state1.value).toBe(!testState.value); expect(state2.value).toBe(testState.value); expect(getters1.get()).toBe(!testState.value); expect(getters2.get()).toBe(testState.value); actions1.toggle(); + flush(); expect(state1.value).toBe(testState.value); expect(state2.value).toBe(testState.value); expect(getters1.get()).toBe(testState.value); @@ -100,11 +105,13 @@ describe("createFluxFactory", () => { expect(getters1.get()).toBe(!testState.value); expect(getters2.get()).toBe(!testState.value); actions2.toggle(); + flush(); expect(state1.value).toBe(!testState.value); expect(state2.value).toBe(testState.value); expect(getters1.get()).toBe(!testState.value); expect(getters2.get()).toBe(testState.value); actions1.toggle(); + flush(); expect(state1.value).toBe(testState.value); expect(state2.value).toBe(testState.value); expect(getters1.get()).toBe(testState.value); @@ -119,12 +126,14 @@ describe("createFluxFactory", () => { expect(state2.value).toBe(testState.value); expect(getters1.get()).toBe(testState.value); expect(getters2.get()).toBe(testState.value); - actions2.set("value", !testState.value); + actions2.set(s => { s.value = !testState.value; }); + flush(); expect(state1.value).toBe(testState.value); expect(state2.value).toBe(!testState.value); expect(getters1.get()).toBe(testState.value); expect(getters2.get()).toBe(!testState.value); - actions1.set("value", !testState.value); + actions1.set(s => { s.value = !testState.value; }); + flush(); expect(state1.value).toBe(!testState.value); expect(state2.value).toBe(!testState.value); expect(getters1.get()).toBe(!testState.value); @@ -139,12 +148,14 @@ describe("createFluxFactory", () => { expect(state2.value).toBe(testState.value); expect(getters1.get()).toBe(testState.value); expect(getters2.get()).toBe(testState.value); - actions2.set(produce(s => (s.value = !s.value))); + actions2.set(s => { s.value = !s.value; }); + flush(); expect(state1.value).toBe(testState.value); expect(state2.value).toBe(!testState.value); expect(getters1.get()).toBe(testState.value); expect(getters2.get()).toBe(!testState.value); - actions1.set(produce(s => (s.value = !s.value))); + actions1.set(s => { s.value = !s.value; }); + flush(); expect(state1.value).toBe(!testState.value); expect(state2.value).toBe(!testState.value); expect(getters1.get()).toBe(!testState.value); @@ -156,7 +167,8 @@ describe("createFluxFactory", () => { expect(state.value).toBe(testState.value); expect(getters.get()).toBe(testState.value); - actions.set("value", !testState.value); + actions.set(s => { s.value = !testState.value; }); + flush(); expect(state.value).toBe(!testState.value); expect(getters.get()).toBe(!testState.value); expect(state.id).toBe(id); @@ -168,7 +180,8 @@ describe("createFluxFactory", () => { expect(state.value).toBe(testState.value); expect(getters.get()).toBe(testState.value); - actions.set(produce(s => (s.value = !s.value))); + actions.set(s => { s.value = !s.value; }); + flush(); expect(state.value).toBe(!testState.value); expect(getters.get()).toBe(!testState.value); expect(state.id).toBe(id); @@ -181,9 +194,9 @@ describe("createFluxStore", () => { test("get default state value", () => { const { state, getters } = createFluxStore(getInitialState(), { - actions: (setState, state) => ({ + actions: setState => ({ set: setState, - toggle: () => setState("value", !state.value), + toggle: () => setState(s => { s.value = !s.value; }), }), getters: state => ({ get: () => state.value, @@ -197,9 +210,9 @@ describe("createFluxStore", () => { test("toggle state value with actions", () => { const { state, getters, actions } = createFluxStore(getInitialState(), { - actions: (setState, state) => ({ + actions: setState => ({ set: setState, - toggle: () => setState("value", !state.value), + toggle: () => setState(s => { s.value = !s.value; }), }), getters: state => ({ get: () => state.value, @@ -212,6 +225,7 @@ describe("createFluxStore", () => { expect(state.id).toBe(id); expect(getters.getId()).toBe(id); actions.toggle(); + flush(); expect(state.value).toBe(!testState.value); expect(getters.get()).toBe(!testState.value); expect(state.id).toBe(id); @@ -220,9 +234,9 @@ describe("createFluxStore", () => { test("manually change state value with actions", () => { const { state, getters, actions } = createFluxStore(getInitialState(), { - actions: (setState, state) => ({ + actions: setState => ({ set: setState, - toggle: () => setState("value", !state.value), + toggle: () => setState(s => { s.value = !s.value; }), }), getters: state => ({ get: () => state.value, @@ -234,7 +248,8 @@ describe("createFluxStore", () => { expect(getters.get()).toBe(testState.value); expect(state.id).toBe(id); expect(getters.getId()).toBe(id); - actions.set("value", !testState.value); + actions.set(s => { s.value = !testState.value; }); + flush(); expect(state.value).toBe(!testState.value); expect(getters.get()).toBe(!testState.value); expect(state.id).toBe(id); @@ -243,9 +258,9 @@ describe("createFluxStore", () => { test("locally change state value with actions", () => { const { state, getters, actions } = createFluxStore(getInitialState(), { - actions: (setState, state) => ({ + actions: setState => ({ set: setState, - toggle: () => setState("value", !state.value), + toggle: () => setState(s => { s.value = !s.value; }), }), getters: state => ({ get: () => state.value, @@ -255,7 +270,8 @@ describe("createFluxStore", () => { expect(state.value).toBe(testState.value); expect(getters.get()).toBe(testState.value); - actions.set(produce(s => (s.value = !s.value))); + actions.set(s => { s.value = !s.value; }); + flush(); expect(state.value).toBe(!testState.value); expect(getters.get()).toBe(!testState.value); expect(state.id).toBe(id); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b7a083eac..fbaead4ae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -361,8 +361,8 @@ importers: packages/flux-store: devDependencies: solid-js: - specifier: ^1.9.7 - version: 1.9.7 + specifier: 2.0.0-beta.14 + version: 2.0.0-beta.14 packages/fullscreen: dependencies: @@ -2722,6 +2722,9 @@ packages: '@solidjs/signals@2.0.0-beta.13': resolution: {integrity: sha512-jc+wLRK+eyUFerH8Mjed4HikdJwz3z95TT7tqyG+K00IV9jfgZWvR1nZDUEZc6kXffmtW/z+w6PNn62ea5KjIw==} + '@solidjs/signals@2.0.0-beta.14': + resolution: {integrity: sha512-y72nYtD7ogwX/UR5g2Y+meyeO6Q/xbQGtmvVTQX6USkMwEGOMnytqDnHj5amUzD7Fzqg32svwtCSx/q8hsOXAA==} + '@solidjs/web@2.0.0-beta.10': resolution: {integrity: sha512-Ox7MBv19kuxHoHhWoLCCcc6aykSgaqzWvWT7RB66VqlFnQ8Lid2ncd30g5L4XC0GB+MN/WZVb68tiYrAFUDIAg==} peerDependencies: @@ -5355,6 +5358,9 @@ packages: solid-js@2.0.0-beta.13: resolution: {integrity: sha512-uAknr7Xkn25zAufBrYko4eOCbcg/gkrwnmE9KVb2Kb3vVZw2ibqseNxpjslnwJkT4gFScmFniqJtzRp7vO2klA==} + solid-js@2.0.0-beta.14: + resolution: {integrity: sha512-gbbvlxhs1GgL1IsnwHNtkTCRBBQcIDMwznBw3T05iYvP+fuUKMyIPku+ZLjeALyX4RaSLR99JSL6NttyHsYb8Q==} + solid-refresh@0.8.0-next.7: resolution: {integrity: sha512-fqkPRAeiE0tqfo2ZljeQBIXwfYssU2w1FmaWFrXmnV33B/CfGfez7BjtOF0Y1/orUNRXI/DZcJlJThHllcCMsA==} peerDependencies: @@ -7747,6 +7753,8 @@ snapshots: '@solidjs/signals@2.0.0-beta.13': {} + '@solidjs/signals@2.0.0-beta.14': {} + '@solidjs/web@2.0.0-beta.10(solid-js@2.0.0-beta.10)': dependencies: seroval: 1.5.4 @@ -10829,8 +10837,15 @@ snapshots: dependencies: '@solidjs/signals': 2.0.0-beta.13 csstype: 3.1.3 - seroval: 1.5.2 - seroval-plugins: 1.5.2(seroval@1.5.2) + seroval: 1.5.4 + seroval-plugins: 1.5.4(seroval@1.5.4) + + solid-js@2.0.0-beta.14: + dependencies: + '@solidjs/signals': 2.0.0-beta.14 + csstype: 3.1.3 + seroval: 1.5.4 + seroval-plugins: 1.5.4(seroval@1.5.4) solid-refresh@0.8.0-next.7(solid-js@2.0.0-beta.10): dependencies: