diff --git a/packages/solid/src/reactive/signal.ts b/packages/solid/src/reactive/signal.ts index 6108ef2cd..9a37eabfd 100644 --- a/packages/solid/src/reactive/signal.ts +++ b/packages/solid/src/reactive/signal.ts @@ -35,8 +35,8 @@ let rootCount = 0; const [transPending, setTransPending] = /*@__PURE__*/ createSignal(false); declare global { - var _$afterUpdate: () => void; - var _$afterCreateRoot: (root: Owner) => void; + var _$afterUpdate: (() => void) | undefined; + var _$afterCreateRoot: ((root: Owner) => void) | undefined; } export interface SignalState { diff --git a/packages/solid/store/src/index.ts b/packages/solid/store/src/index.ts index 8b68989f2..658562b5b 100644 --- a/packages/solid/store/src/index.ts +++ b/packages/solid/store/src/index.ts @@ -14,3 +14,7 @@ export type { } from "./store.js"; export * from "./mutable.js"; export * from "./modifiers.js"; + +// dev +import { $NAME, $NODE, isWrappable } from "./store.js"; +export const DEV = "_SOLID_DEV_" ? ({ $NAME, $NODE, isWrappable } as const) : undefined; diff --git a/packages/solid/store/src/modifiers.ts b/packages/solid/store/src/modifiers.ts index 6d6fa2874..dd1949765 100644 --- a/packages/solid/store/src/modifiers.ts +++ b/packages/solid/store/src/modifiers.ts @@ -117,7 +117,7 @@ export function reconcile( return state => { if (!isWrappable(state) || !isWrappable(v)) return v; const res = applyState(v, { [$ROOT]: state }, $ROOT, merge, key); - return res === undefined ? state as T : res as T; + return res === undefined ? (state as T) : res; }; } @@ -152,7 +152,7 @@ export function produce(fn: (state: T) => void): (state: T) => T { if (!(proxy = producers.get(state as Record))) { producers.set( state as Record, - (proxy = new Proxy(state, setterTraps)) + (proxy = new Proxy(state as Extract, setterTraps)) ); } fn(proxy); diff --git a/packages/solid/store/src/server.ts b/packages/solid/store/src/server.ts index cb3875bd1..36487f695 100644 --- a/packages/solid/store/src/server.ts +++ b/packages/solid/store/src/server.ts @@ -135,3 +135,5 @@ export function produce(fn: (state: T) => void): (state: T) => T { return state; }; } + +export const DEV = undefined; diff --git a/packages/solid/store/src/store.ts b/packages/solid/store/src/store.ts index c0b565d62..6fb76992b 100644 --- a/packages/solid/store/src/store.ts +++ b/packages/solid/store/src/store.ts @@ -1,9 +1,33 @@ -import { getListener, batch, DEV, $PROXY, $TRACK, Accessor, createSignal } from "solid-js"; +import { getListener, batch, DEV, $PROXY, $TRACK, createSignal } from "solid-js"; + export const $RAW = Symbol("store-raw"), $NODE = Symbol("store-node"), $NAME = Symbol("store-name"); -export type StoreNode = Record; +// dev +declare global { + var _$onStoreNodeUpdate: OnStoreNodeUpdate | undefined; +} + +type DataNode = { + (): any; + $(value?: any): void; +}; +type DataNodes = Record; + +export type OnStoreNodeUpdate = ( + state: StoreNode, + property: PropertyKey, + value: StoreNode | NotWrappable, + prev: StoreNode | NotWrappable +) => void; + +export interface StoreNode { + [$NAME]?: string; + [$NODE]?: DataNodes; + [key: PropertyKey]: any; +} + export namespace SolidStore { export interface Unwrappable {} } @@ -54,6 +78,17 @@ export function isWrappable(obj: any) { ); } +/** + * Returns the underlying data in the store without a proxy. + * @param item store proxy object + * @example + * ```js + * const initial = {z...}; + * const [state, setState] = createStore(initial); + * initial === state; // => false + * initial === unwrap(state); // => true + * ``` + */ export function unwrap(item: T, set?: Set): T; export function unwrap(item: any, set = new Set()): T { let result, unwrapped, v, prop; @@ -74,7 +109,7 @@ export function unwrap(item: any, set = new Set()): T { desc = Object.getOwnPropertyDescriptors(item); for (let i = 0, l = keys.length; i < l; i++) { prop = keys[i]; - if ((desc as any)[prop].get) continue; + if (desc[prop].get) continue; v = item[prop]; if ((unwrapped = unwrap(v, set)) !== v) item[prop] = unwrapped; } @@ -82,14 +117,14 @@ export function unwrap(item: any, set = new Set()): T { return item; } -export function getDataNodes(target: StoreNode) { +export function getDataNodes(target: StoreNode): DataNodes { let nodes = target[$NODE]; if (!nodes) Object.defineProperty(target, $NODE, { value: (nodes = {}) }); return nodes; } -export function getDataNode(nodes: Record, property: string | symbol, value: any) { - return nodes[property as string] || (nodes[property as string] = createDataNode(value)); +export function getDataNode(nodes: DataNodes, property: PropertyKey, value: any) { + return nodes[property] || (nodes[property] = createDataNode(value)); } export function proxyDescriptor(target: StoreNode, property: PropertyKey) { @@ -126,8 +161,8 @@ function createDataNode(value?: any) { equals: false, internal: true }); - (s as Accessor & { $: (v: any) => void }).$ = set; - return s as Accessor & { $: (v: any) => void }; + (s as DataNode).$ = set; + return s as DataNode; } const proxyTraps: ProxyHandler = { @@ -191,16 +226,19 @@ export function setProperty( property: PropertyKey, value: any, deleting: boolean = false -) { +): void { if (!deleting && state[property] === value) return; - const prev = state[property]; - const len = state.length; - if (value === undefined) { - delete state[property]; - } else state[property] = value; + const prev = state[property], + len = state.length; + + if ("_SOLID_DEV_" && globalThis._$onStoreNodeUpdate) + globalThis._$onStoreNodeUpdate(state, property, value, prev); + + if (value === undefined) delete state[property]; + else state[property] = value; let nodes = getDataNodes(state), - node; - if ((node = getDataNode(nodes, property as string, prev))) node.$(() => value); + node: DataNode; + if ((node = getDataNode(nodes, property, prev))) node.$(() => value); if (Array.isArray(state) && state.length !== len) (node = getDataNode(nodes, "length", len)) && node.$(state.length); diff --git a/packages/solid/test/dev.spec.ts b/packages/solid/test/dev.spec.ts index c04c93ed5..8126457b8 100644 --- a/packages/solid/test/dev.spec.ts +++ b/packages/solid/test/dev.spec.ts @@ -9,7 +9,7 @@ import { Owner, createContext } from "../src"; -import { createStore } from "../store/src"; +import { createStore, unwrap } from "../store/src"; describe("Dev features", () => { test("Reactive graph serialization", () => { @@ -122,4 +122,17 @@ describe("Dev features", () => { }); }); }); + + test("OnStoreNodeUpdate Hook", () => { + const cb = jest.fn(); + global._$onStoreNodeUpdate = cb; + const [s, set] = createStore({ firstName: "John", lastName: "Smith", inner: { foo: 1 } }); + expect(cb).toHaveBeenCalledTimes(0); + set({ firstName: "Matt" }); + expect(cb).toHaveBeenCalledTimes(1); + expect(cb).toHaveBeenCalledWith(unwrap(s), "firstName", "Matt", "John"); + set("inner", "foo", 2); + expect(cb).toHaveBeenCalledTimes(2); + expect(cb).toHaveBeenCalledWith(unwrap(s.inner), "foo", 2, 1); + }); });