@pixotope/react-context-store 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +10 -10
- package/.turbo/turbo-check-types.log +1 -1
- package/.turbo/turbo-test.log +4 -4
- package/CHANGELOG.md +14 -0
- package/README.md +69 -0
- package/dist/index.cjs +42 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -2
- package/dist/index.d.ts +114 -2
- package/dist/index.js +42 -96
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/store.tsx +182 -23
- package/typedoc.json +3 -0
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* A function used to compare two versions of selected state, to decide
|
|
5
|
+
* whether subscribers should be notified of a change.
|
|
6
|
+
* @param first - The previously selected state.
|
|
7
|
+
* @param second - The newly selected state.
|
|
8
|
+
* @returns `true` if the two values should be treated as equal (no update triggered), `false` otherwise.
|
|
9
|
+
*/
|
|
3
10
|
type SelectorOptions<Selected> = (first: Selected, second: Selected) => boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Options that configure the behavior of a store created with {@link createContextStore}.
|
|
13
|
+
*/
|
|
4
14
|
type ContextOptions = {
|
|
5
15
|
/**
|
|
6
16
|
* If true, the store state will be preserved across mounts and un-mounts
|
|
@@ -14,42 +24,144 @@ type ContextOptions = {
|
|
|
14
24
|
*/
|
|
15
25
|
compare?: SelectorOptions<unknown>;
|
|
16
26
|
};
|
|
27
|
+
/**
|
|
28
|
+
* The value accepted by a store's `set` function: either the next state
|
|
29
|
+
* directly, or an updater function that receives the previous state and
|
|
30
|
+
* returns the next state.
|
|
31
|
+
*/
|
|
17
32
|
type SetterArgs<Store> = Store | ((prev: Store) => Store);
|
|
33
|
+
/** Maps an actions map (as passed to {@link createContextStore}) to the shape of the object returned by `useActions`. */
|
|
18
34
|
type ExtractActionKeys<T> = {
|
|
19
35
|
[K in keyof T]: T[K] extends (stateProps: never, action: infer A) => void | Promise<void> ? A extends ActionablePayload<infer Payload> ? Payload extends undefined ? () => void : (payload: Payload) => void : () => void : never;
|
|
20
36
|
};
|
|
37
|
+
/** Flattens an intersection/mapped type into a single object type, for cleaner hover tooltips. */
|
|
21
38
|
type Prettify<T> = {
|
|
22
39
|
[K in keyof T]: T[K];
|
|
23
40
|
} & {};
|
|
41
|
+
/**
|
|
42
|
+
* The action argument passed to an action handler, wrapping its payload.
|
|
43
|
+
* Use this as the type of an action's second parameter to give the
|
|
44
|
+
* corresponding {@link createContextStore} action a typed payload, which is
|
|
45
|
+
* then reflected in the function returned by `useActions`.
|
|
46
|
+
* @typeParam Payload - The type of the action's payload. Defaults to `any`.
|
|
47
|
+
* @example
|
|
48
|
+
* ```tsx
|
|
49
|
+
* const store = createContextStore(initialState, {
|
|
50
|
+
* incrementBy: ({ set, get }, action: ActionablePayload<number>) => {
|
|
51
|
+
* set((prev) => ({ ...prev, count: get().count + action.payload }));
|
|
52
|
+
* },
|
|
53
|
+
* });
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
24
56
|
type ActionablePayload<Payload = any> = {
|
|
25
57
|
payload: Payload;
|
|
26
58
|
};
|
|
59
|
+
/** The state accessors passed as the first argument to every action handler. */
|
|
27
60
|
type StateActionProps<Store> = {
|
|
61
|
+
/** Updates the store state. */
|
|
28
62
|
set: (value: SetterArgs<Store>) => void;
|
|
63
|
+
/** Returns the current store state. */
|
|
29
64
|
get: () => Store;
|
|
30
65
|
};
|
|
66
|
+
/**
|
|
67
|
+
* A map of action names to action handlers, as passed to {@link createContextStore}.
|
|
68
|
+
* Each handler receives {@link StateActionProps} for reading/writing the
|
|
69
|
+
* store, plus an {@link ActionablePayload} carrying any payload passed by the caller.
|
|
70
|
+
*/
|
|
31
71
|
type Actions<Store> = {
|
|
32
72
|
[key: string]: (stateProps: StateActionProps<Store>, action: ActionablePayload) => void | Promise<void>;
|
|
33
73
|
};
|
|
74
|
+
/** The object returned by a store's `useStore` hook. */
|
|
34
75
|
type UseStoreReturnType<Store, SelectorOutput> = {
|
|
76
|
+
/** The currently selected state. Reading this subscribes the component to updates. */
|
|
35
77
|
state: SelectorOutput;
|
|
78
|
+
/** Returns the currently selected state. Equivalent to reading `state`. */
|
|
36
79
|
get: () => SelectorOutput;
|
|
80
|
+
/** Updates the store state. */
|
|
37
81
|
set: (value: SetterArgs<Store>) => void;
|
|
82
|
+
/** Returns the entire store state, not just the selected slice. */
|
|
38
83
|
selector: () => Store;
|
|
39
84
|
};
|
|
85
|
+
/**
|
|
86
|
+
* The object returned by {@link createContextStore}: the `Provider`
|
|
87
|
+
* component plus the hooks and functions used to read, update and
|
|
88
|
+
* subscribe to the store.
|
|
89
|
+
*/
|
|
40
90
|
type ContextReturnType<Store, A extends Actions<Store>> = {
|
|
91
|
+
/** Wraps the part of the component tree that should have access to the store. */
|
|
41
92
|
Provider: React.FC<React.PropsWithChildren>;
|
|
93
|
+
/**
|
|
94
|
+
* Reads (a selection of) the store state from the nearest `Provider`,
|
|
95
|
+
* re-rendering the component whenever the selected state changes.
|
|
96
|
+
* @param selector - Selects the part of the store the component needs. Defaults to the entire store.
|
|
97
|
+
* @param options - A function used to compare the previous and next selected state. Defaults to the store's `compare` option, or shallow equality.
|
|
98
|
+
* @returns The selected state, plus functions to read and update the store.
|
|
99
|
+
*/
|
|
42
100
|
useStore: <SelectorOutput = Store>(selector?: (store: Store) => SelectorOutput, options?: SelectorOptions<SelectorOutput>) => UseStoreReturnType<Store, SelectorOutput>;
|
|
101
|
+
/**
|
|
102
|
+
* Returns the actions passed to {@link createContextStore}, bound to the
|
|
103
|
+
* current store. Actions declared with an {@link ActionablePayload}
|
|
104
|
+
* parameter take a single payload argument; other actions take none.
|
|
105
|
+
*/
|
|
43
106
|
useActions: () => Prettify<ExtractActionKeys<A>>;
|
|
107
|
+
/**
|
|
108
|
+
* Returns a function to update the store state from the nearest
|
|
109
|
+
* `Provider`, without subscribing the component to store updates.
|
|
110
|
+
*/
|
|
44
111
|
useSetStore: () => (value: SetterArgs<Store>) => void;
|
|
45
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Subscribes to store updates from outside the React component tree.
|
|
114
|
+
* @param selector - Selects the part of the store to watch.
|
|
115
|
+
* @param callback - Called with the selected state whenever it changes.
|
|
116
|
+
* @param options - A function used to compare the previous and next selected state. Defaults to shallow equality.
|
|
117
|
+
* @returns A function to remove the subscription.
|
|
118
|
+
*/
|
|
46
119
|
subscribe: <SelectorOutput = Store>(selector: (store: Store) => SelectorOutput, callback: (state: SelectorOutput) => void, options?: SelectorOptions<SelectorOutput>) => () => void;
|
|
120
|
+
/** Removes a subscription created with `subscribe`. */
|
|
47
121
|
unsubscribe: (callback: () => void) => void;
|
|
48
122
|
};
|
|
123
|
+
/**
|
|
124
|
+
* Creates a React context-based store: a `Provider` component plus hooks to
|
|
125
|
+
* read, update and subscribe to its state.
|
|
126
|
+
*
|
|
127
|
+
* Each call creates an independent store with its own React context, so the
|
|
128
|
+
* `Provider` and hooks returned by one call must always be used together.
|
|
129
|
+
* @typeParam Store - The shape of the store state.
|
|
130
|
+
* @typeParam A - The map of named actions available to update the store.
|
|
131
|
+
* @param args - Positional arguments: the `initialState` the store is initialized with, an
|
|
132
|
+
* optional `actions` map of named action handlers used to update the store (see
|
|
133
|
+
* {@link ActionablePayload} for actions that take a payload), and optional `options` to
|
|
134
|
+
* customize the store's behavior.
|
|
135
|
+
* @returns An object with a `Provider` component and hooks to read, update and subscribe to the store's state. See {@link ContextReturnType}.
|
|
136
|
+
* @example
|
|
137
|
+
* ```tsx
|
|
138
|
+
* const store = createContextStore(
|
|
139
|
+
* { count: 0 },
|
|
140
|
+
* {
|
|
141
|
+
* increment: ({ set, get }) => set({ ...get(), count: get().count + 1 }),
|
|
142
|
+
* }
|
|
143
|
+
* );
|
|
144
|
+
*
|
|
145
|
+
* function Counter() {
|
|
146
|
+
* const { state: count } = store.useStore((state) => state.count);
|
|
147
|
+
* const { increment } = store.useActions();
|
|
148
|
+
*
|
|
149
|
+
* return <button onClick={() => increment()}>{count}</button>;
|
|
150
|
+
* }
|
|
151
|
+
*
|
|
152
|
+
* function App() {
|
|
153
|
+
* return (
|
|
154
|
+
* <store.Provider>
|
|
155
|
+
* <Counter />
|
|
156
|
+
* </store.Provider>
|
|
157
|
+
* );
|
|
158
|
+
* }
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
49
161
|
declare function createContextStore<Store, A extends Actions<Store> = Actions<Store>>(...args: Extract<A, {
|
|
50
162
|
payload: A;
|
|
51
163
|
}> extends {
|
|
52
164
|
payload: infer Payload;
|
|
53
165
|
} ? [initialState: Store, actions?: A, options?: ContextOptions] : [initialState: Store, options?: ContextOptions]): ContextReturnType<Store, A>;
|
|
54
166
|
|
|
55
|
-
export { type ActionablePayload, type ContextOptions, type SelectorOptions, type SetterArgs, createContextStore };
|
|
167
|
+
export { type ActionablePayload, type Actions, type ContextOptions, type ContextReturnType, type ExtractActionKeys, type Prettify, type SelectorOptions, type SetterArgs, type StateActionProps, type UseStoreReturnType, createContextStore };
|
package/dist/index.js
CHANGED
|
@@ -1,96 +1,4 @@
|
|
|
1
|
-
var __create = Object.create;
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
-
var __commonJS = (cb, mod) => function __require() {
|
|
8
|
-
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
9
|
-
};
|
|
10
|
-
var __copyProps = (to, from, except, desc) => {
|
|
11
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
-
for (let key of __getOwnPropNames(from))
|
|
13
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
19
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
20
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
21
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
22
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
23
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
24
|
-
mod
|
|
25
|
-
));
|
|
26
|
-
|
|
27
|
-
// ../utils/dist/comparison/index.js
|
|
28
|
-
var require_comparison = __commonJS({
|
|
29
|
-
"../utils/dist/comparison/index.js"(exports, module) {
|
|
30
|
-
"use strict";
|
|
31
|
-
var __defProp2 = Object.defineProperty;
|
|
32
|
-
var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
|
|
33
|
-
var __getOwnPropNames2 = Object.getOwnPropertyNames;
|
|
34
|
-
var __hasOwnProp2 = Object.prototype.hasOwnProperty;
|
|
35
|
-
var __export = (target, all) => {
|
|
36
|
-
for (var name in all)
|
|
37
|
-
__defProp2(target, name, { get: all[name], enumerable: true });
|
|
38
|
-
};
|
|
39
|
-
var __copyProps2 = (to, from, except, desc) => {
|
|
40
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
41
|
-
for (let key of __getOwnPropNames2(from))
|
|
42
|
-
if (!__hasOwnProp2.call(to, key) && key !== except)
|
|
43
|
-
__defProp2(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc2(from, key)) || desc.enumerable });
|
|
44
|
-
}
|
|
45
|
-
return to;
|
|
46
|
-
};
|
|
47
|
-
var __toCommonJS = (mod) => __copyProps2(__defProp2({}, "__esModule", { value: true }), mod);
|
|
48
|
-
var comparison_exports = {};
|
|
49
|
-
__export(comparison_exports, {
|
|
50
|
-
shallowEqual: () => shallowEqual2
|
|
51
|
-
});
|
|
52
|
-
module.exports = __toCommonJS(comparison_exports);
|
|
53
|
-
function shallowEqual2(objA, objB) {
|
|
54
|
-
if (Object.is(objA, objB)) {
|
|
55
|
-
return true;
|
|
56
|
-
}
|
|
57
|
-
if (typeof objA !== "object" || objA === null || typeof objB !== "object" || objB === null) {
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
if (objA instanceof Map && objB instanceof Map) {
|
|
61
|
-
if (objA.size !== objB.size) return false;
|
|
62
|
-
for (const [key, value] of objA) {
|
|
63
|
-
if (!Object.is(value, objB.get(key))) {
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return true;
|
|
68
|
-
}
|
|
69
|
-
if (objA instanceof Set && objB instanceof Set) {
|
|
70
|
-
if (objA.size !== objB.size) return false;
|
|
71
|
-
for (const value of objA) {
|
|
72
|
-
if (!objB.has(value)) {
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
return true;
|
|
77
|
-
}
|
|
78
|
-
const keysA = Object.keys(objA);
|
|
79
|
-
if (keysA.length !== Object.keys(objB).length) {
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
for (let i = 0; i < keysA.length; i++) {
|
|
83
|
-
if (!Object.prototype.hasOwnProperty.call(objB, keysA[i]) || !Object.is(objA[keysA[i]], objB[keysA[i]])) {
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return true;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
|
|
92
1
|
// src/store.tsx
|
|
93
|
-
var import_comparison = __toESM(require_comparison(), 1);
|
|
94
2
|
import React, {
|
|
95
3
|
createContext,
|
|
96
4
|
useCallback,
|
|
@@ -99,6 +7,46 @@ import React, {
|
|
|
99
7
|
useRef,
|
|
100
8
|
useSyncExternalStore
|
|
101
9
|
} from "react";
|
|
10
|
+
|
|
11
|
+
// ../utils/dist/chunk-5QIHMQPV.js
|
|
12
|
+
function shallowEqual(objA, objB) {
|
|
13
|
+
if (Object.is(objA, objB)) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
if (typeof objA !== "object" || objA === null || typeof objB !== "object" || objB === null) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
if (objA instanceof Map && objB instanceof Map) {
|
|
20
|
+
if (objA.size !== objB.size) return false;
|
|
21
|
+
for (const [key, value] of objA) {
|
|
22
|
+
if (!Object.is(value, objB.get(key))) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
if (objA instanceof Set && objB instanceof Set) {
|
|
29
|
+
if (objA.size !== objB.size) return false;
|
|
30
|
+
for (const value of objA) {
|
|
31
|
+
if (!objB.has(value)) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
const keysA = Object.keys(objA);
|
|
38
|
+
if (keysA.length !== Object.keys(objB).length) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
for (let i = 0; i < keysA.length; i++) {
|
|
42
|
+
if (!Object.prototype.hasOwnProperty.call(objB, keysA[i]) || !Object.is(objA[keysA[i]], objB[keysA[i]])) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/store.tsx
|
|
102
50
|
var LIB_NAME = "@pixotope/react-context-store";
|
|
103
51
|
function isFunction(value) {
|
|
104
52
|
return typeof value === "function";
|
|
@@ -141,7 +89,7 @@ function createContextStore(initialState, actions = {}, options = {}) {
|
|
|
141
89
|
);
|
|
142
90
|
}
|
|
143
91
|
const observers = /* @__PURE__ */ new Set();
|
|
144
|
-
function subscribeExternal(selector = (store) => store, callback, compare =
|
|
92
|
+
function subscribeExternal(selector = (store) => store, callback, compare = shallowEqual) {
|
|
145
93
|
let lastSelectedState;
|
|
146
94
|
const shouldSendUpdates = (newSelectedState) => {
|
|
147
95
|
if (lastSelectedState === void 0 || !compare(lastSelectedState, newSelectedState)) {
|
|
@@ -169,7 +117,7 @@ function createContextStore(initialState, actions = {}, options = {}) {
|
|
|
169
117
|
observers.forEach((callback) => callback(state));
|
|
170
118
|
}
|
|
171
119
|
};
|
|
172
|
-
function useStore(selector = (store) => store, compare = ((_a) => (_a = options.compare) != null ? _a :
|
|
120
|
+
function useStore(selector = (store) => store, compare = ((_a) => (_a = options.compare) != null ? _a : shallowEqual)()) {
|
|
173
121
|
const store = useContext(StoreContext);
|
|
174
122
|
const lastSelectedState = useRef(void 0);
|
|
175
123
|
if (!store) {
|
|
@@ -229,13 +177,11 @@ function createContextStore(initialState, actions = {}, options = {}) {
|
|
|
229
177
|
return actionProxy;
|
|
230
178
|
}
|
|
231
179
|
const useSetStore = () => useStore(() => false).set;
|
|
232
|
-
const useStoreSelector = () => useStore(() => false).selector;
|
|
233
180
|
return {
|
|
234
181
|
Provider,
|
|
235
182
|
useStore,
|
|
236
183
|
useSetStore,
|
|
237
184
|
useActions,
|
|
238
|
-
useStoreSelector,
|
|
239
185
|
subscribe: observable.subscribe,
|
|
240
186
|
unsubscribe: observable.unsubscribe
|
|
241
187
|
};
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../utils/dist/comparison/index.js","../src/store.tsx"],"sourcesContent":["\"use strict\";\nvar __defProp = Object.defineProperty;\nvar __getOwnPropDesc = Object.getOwnPropertyDescriptor;\nvar __getOwnPropNames = Object.getOwnPropertyNames;\nvar __hasOwnProp = Object.prototype.hasOwnProperty;\nvar __export = (target, all) => {\n for (var name in all)\n __defProp(target, name, { get: all[name], enumerable: true });\n};\nvar __copyProps = (to, from, except, desc) => {\n if (from && typeof from === \"object\" || typeof from === \"function\") {\n for (let key of __getOwnPropNames(from))\n if (!__hasOwnProp.call(to, key) && key !== except)\n __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });\n }\n return to;\n};\nvar __toCommonJS = (mod) => __copyProps(__defProp({}, \"__esModule\", { value: true }), mod);\n\n// src/comparison/index.ts\nvar comparison_exports = {};\n__export(comparison_exports, {\n shallowEqual: () => shallowEqual\n});\nmodule.exports = __toCommonJS(comparison_exports);\n\n// src/comparison/shallowEqual.ts\nfunction shallowEqual(objA, objB) {\n if (Object.is(objA, objB)) {\n return true;\n }\n if (typeof objA !== \"object\" || objA === null || typeof objB !== \"object\" || objB === null) {\n return false;\n }\n if (objA instanceof Map && objB instanceof Map) {\n if (objA.size !== objB.size) return false;\n for (const [key, value] of objA) {\n if (!Object.is(value, objB.get(key))) {\n return false;\n }\n }\n return true;\n }\n if (objA instanceof Set && objB instanceof Set) {\n if (objA.size !== objB.size) return false;\n for (const value of objA) {\n if (!objB.has(value)) {\n return false;\n }\n }\n return true;\n }\n const keysA = Object.keys(objA);\n if (keysA.length !== Object.keys(objB).length) {\n return false;\n }\n for (let i = 0; i < keysA.length; i++) {\n if (!Object.prototype.hasOwnProperty.call(objB, keysA[i]) || !Object.is(objA[keysA[i]], objB[keysA[i]])) {\n return false;\n }\n }\n return true;\n}\n// Annotate the CommonJS export names for ESM import in node:\n0 && (module.exports = {\n shallowEqual\n});\n","import React, {\n createContext,\n useCallback,\n useContext,\n useMemo,\n useRef,\n useSyncExternalStore,\n} from \"react\";\nimport { shallowEqual } from \"@pixotope/utils/comparison\";\n\nconst LIB_NAME = \"@pixotope/react-context-store\";\n\nexport type SelectorOptions<Selected> = (\n first: Selected,\n second: Selected\n) => boolean;\n\nexport type ContextOptions = {\n /**\n * If true, the store state will be preserved across mounts and un-mounts\n * of the Provider.\n * @default false\n */\n global?: boolean;\n /**\n * A function that will be used to compare the selected state.\n * @default shallowEqual\n */\n compare?: SelectorOptions<unknown>;\n};\n\nexport type SetterArgs<Store> = Store | ((prev: Store) => Store);\ntype ExtractActionKeys<T> = {\n [K in keyof T]: T[K] extends (\n stateProps: never,\n action: infer A\n ) => void | Promise<void>\n ? A extends ActionablePayload<infer Payload>\n ? Payload extends undefined\n ? () => void\n : (payload: Payload) => void\n : () => void\n : never;\n};\n\ntype Prettify<T> = {\n [K in keyof T]: T[K];\n} & {};\n\nexport type ActionablePayload<Payload = any> = {\n payload: Payload;\n};\n\ntype StateActionProps<Store> = {\n set: (value: SetterArgs<Store>) => void;\n get: () => Store;\n};\n\ntype Actions<Store> = {\n [key: string]: (\n stateProps: StateActionProps<Store>,\n action: ActionablePayload\n ) => void | Promise<void>;\n};\n\ntype UseStoreReturnType<Store, SelectorOutput> = {\n state: SelectorOutput;\n get: () => SelectorOutput;\n set: (value: SetterArgs<Store>) => void;\n selector: () => Store;\n};\n\ntype ContextReturnType<Store, A extends Actions<Store>> = {\n Provider: React.FC<React.PropsWithChildren>;\n useStore: <SelectorOutput = Store>(\n selector?: (store: Store) => SelectorOutput,\n options?: SelectorOptions<SelectorOutput>\n ) => UseStoreReturnType<Store, SelectorOutput>;\n useActions: () => Prettify<ExtractActionKeys<A>>;\n useSetStore: () => (value: SetterArgs<Store>) => void;\n useStoreSelector: () => () => Store;\n subscribe: <SelectorOutput = Store>(\n selector: (store: Store) => SelectorOutput,\n callback: (state: SelectorOutput) => void,\n options?: SelectorOptions<SelectorOutput>\n ) => () => void;\n unsubscribe: (callback: () => void) => void;\n};\n\nfunction isFunction(value: any): value is (prev: any) => any {\n return typeof value === \"function\";\n}\n\nexport function createContextStore<Store, A extends Actions<Store> = Actions<Store>>(\n ...args: Extract<A, { payload: A }> extends { payload: infer Payload }\n ? [initialState: Store, actions?: A, options?: ContextOptions]\n : [initialState: Store, options?: ContextOptions]\n): ContextReturnType<Store, A>;\nexport function createContextStore<Store, A extends Actions<Store> = Actions<Store>>(\n initialState: Store,\n actions: A = {} as A,\n options: ContextOptions = {}\n): ContextReturnType<Store, A> {\n let globalStore: Store | undefined = options.global\n ? initialState\n : undefined;\n\n function useStoreData({\n defaultSubscriber = [],\n }: {\n defaultSubscriber?: ((state: Store) => void)[];\n } = {}): {\n get: () => Store;\n set: (value: SetterArgs<Store>) => void;\n subscribe: (callback: (state: Store) => void) => () => void;\n } {\n const store = useRef<Store>(globalStore ?? initialState);\n\n const get = useCallback(() => store.current, []);\n\n const subscribers = useRef(\n new Set<(state: Store) => void>(defaultSubscriber)\n );\n\n const set = useCallback((value: SetterArgs<Store>) => {\n store.current = isFunction(value) ? value(store.current) : value;\n\n if (options.global) {\n globalStore = store.current;\n }\n\n subscribers.current.forEach((callback) => callback(store.current));\n }, []);\n\n const subscribe = useCallback((callback: (state: Store) => void) => {\n subscribers.current.add(callback);\n\n return () => subscribers.current.delete(callback);\n }, []);\n\n return {\n get,\n set,\n subscribe,\n };\n }\n\n type UseStoreDataReturnType = ReturnType<typeof useStoreData>;\n\n const StoreContext = createContext<UseStoreDataReturnType | null>(null);\n\n /**\n * This is the provider that will be used to wrap the react component tree\n * to provide the store to all the components in the tree.\n */\n function Provider({ children }: React.PropsWithChildren) {\n return (\n <StoreContext.Provider\n value={useStoreData({ defaultSubscriber: [observable.broadcast] })}\n >\n {children}\n </StoreContext.Provider>\n );\n }\n\n /**\n * This holds all the observables that are subscribed to the store\n * but not being actively used by any component. Helps to broadcast\n * store updates to all the observables that are interested in reacting\n * to state outside the react component tree.\n */\n const observers = new Set<(state: Store) => void>();\n\n function subscribeExternal<SelectorOutput = Store>(\n selector: (store: Store) => SelectorOutput = (store) =>\n store as unknown as SelectorOutput,\n callback: (state: SelectorOutput) => void,\n compare: SelectorOptions<SelectorOutput> = shallowEqual\n ): () => void {\n let lastSelectedState: SelectorOutput | undefined;\n\n const shouldSendUpdates = (newSelectedState: SelectorOutput) => {\n if (\n lastSelectedState === undefined ||\n !compare(lastSelectedState, newSelectedState)\n ) {\n lastSelectedState = newSelectedState;\n\n return true;\n }\n\n return false;\n };\n\n const selectedCb = (store: Store) => {\n const selected = selector(store);\n const shouldSend = shouldSendUpdates(selected);\n\n if (shouldSend) {\n callback(selector(store));\n }\n };\n observers.add(selectedCb);\n\n return () => observers.delete(selectedCb);\n }\n\n /**\n * A simple observable that proxy store updates to all the subscribers\n * that are interested in reacting to state outside the react component tree.\n */\n const observable = {\n subscribe: subscribeExternal,\n unsubscribe: (callback: (state: Store) => void) => {\n observers.delete(callback);\n },\n broadcast: (state: Store) => {\n observers.forEach((callback) => callback(state));\n },\n } as const;\n\n /**\n * This is the hook that will be used to access the store from any component\n * in the react component tree.\n * @param selector A function that will be used to select the part of the store\n * that is needed by the component.\n * @param options Options to customize the behavior of the hook.\n * @returns An object with the selected state, a function to update the store\n * and a function to get the entire store.\n * @example\n * ```tsx\n * const { get, set } = useStore(store => store.user);\n * const { get, set } = useStore(store => store.user, { deepEqual: false });\n * const { get, set } = useStore(store => store.user, {\n * compare: (first, second) => first.id === second.id\n * });\n * ```\n */\n function useStore<SelectorOutput = Store>(\n selector: (store: Store) => SelectorOutput = (store) =>\n store as unknown as SelectorOutput,\n compare: SelectorOptions<SelectorOutput> = options.compare ?? shallowEqual\n ): UseStoreReturnType<Store, SelectorOutput> {\n const store = useContext(StoreContext);\n const lastSelectedState = useRef<SelectorOutput | undefined>(undefined);\n\n if (!store) {\n throw new Error(\n `[${LIB_NAME}] Store not found. Make sure the component is wrapped in a ${Provider} component.`\n );\n }\n\n const state = useSyncExternalStore(\n store.subscribe,\n () => {\n const selectedState = selector(store.get());\n\n if (\n lastSelectedState.current === undefined ||\n !compare(lastSelectedState.current, selectedState)\n ) {\n lastSelectedState.current = selectedState;\n }\n\n return lastSelectedState.current;\n },\n () => selector(initialState)\n );\n\n return {\n state,\n get: () => state,\n set: store.set,\n selector: () => store.get(),\n };\n }\n\n function useActions() {\n const store = useContext(StoreContext);\n\n if (!store) {\n throw new Error(\n `[${LIB_NAME}] Store not found. Make sure the component is wrapped in a ${Provider} component.`\n );\n }\n\n const actionProxy = useMemo(\n () =>\n new Proxy(actions, {\n get: (target, prop) => {\n const action = target[prop as string];\n\n if (action) {\n return (args: Parameters<typeof action>[\"1\"]) => {\n action(\n {\n set: store.set,\n get: store.get,\n },\n {\n payload: args,\n }\n );\n };\n }\n },\n set: () => {\n throw new Error(`[${LIB_NAME}] Actions cannot be updated`);\n },\n }),\n []\n );\n\n return actionProxy as unknown as ExtractActionKeys<typeof actions>;\n }\n\n const useSetStore = () => useStore(() => false).set;\n const useStoreSelector = () => useStore(() => false).selector;\n\n return {\n Provider,\n useStore,\n useSetStore,\n useActions,\n useStoreSelector,\n subscribe: observable.subscribe,\n unsubscribe: observable.unsubscribe,\n };\n}\n"],"mappings":"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","names":["__defProp","__getOwnPropDesc","__getOwnPropNames","__hasOwnProp","__copyProps","shallowEqual"]}
|
|
1
|
+
{"version":3,"sources":["../src/store.tsx","../../utils/dist/chunk-5QIHMQPV.js"],"sourcesContent":["import React, {\n createContext,\n useCallback,\n useContext,\n useMemo,\n useRef,\n useSyncExternalStore,\n} from \"react\";\nimport { shallowEqual } from \"@pixotope/utils/comparison\";\n\nconst LIB_NAME = \"@pixotope/react-context-store\";\n\n/**\n * A function used to compare two versions of selected state, to decide\n * whether subscribers should be notified of a change.\n * @param first - The previously selected state.\n * @param second - The newly selected state.\n * @returns `true` if the two values should be treated as equal (no update triggered), `false` otherwise.\n */\nexport type SelectorOptions<Selected> = (\n first: Selected,\n second: Selected\n) => boolean;\n\n/**\n * Options that configure the behavior of a store created with {@link createContextStore}.\n */\nexport type ContextOptions = {\n /**\n * If true, the store state will be preserved across mounts and un-mounts\n * of the Provider.\n * @default false\n */\n global?: boolean;\n /**\n * A function that will be used to compare the selected state.\n * @default shallowEqual\n */\n compare?: SelectorOptions<unknown>;\n};\n\n/**\n * The value accepted by a store's `set` function: either the next state\n * directly, or an updater function that receives the previous state and\n * returns the next state.\n */\nexport type SetterArgs<Store> = Store | ((prev: Store) => Store);\n\n/** Maps an actions map (as passed to {@link createContextStore}) to the shape of the object returned by `useActions`. */\nexport type ExtractActionKeys<T> = {\n [K in keyof T]: T[K] extends (\n stateProps: never,\n action: infer A\n ) => void | Promise<void>\n ? A extends ActionablePayload<infer Payload>\n ? Payload extends undefined\n ? () => void\n : (payload: Payload) => void\n : () => void\n : never;\n};\n\n/** Flattens an intersection/mapped type into a single object type, for cleaner hover tooltips. */\nexport type Prettify<T> = {\n [K in keyof T]: T[K];\n} & {};\n\n/**\n * The action argument passed to an action handler, wrapping its payload.\n * Use this as the type of an action's second parameter to give the\n * corresponding {@link createContextStore} action a typed payload, which is\n * then reflected in the function returned by `useActions`.\n * @typeParam Payload - The type of the action's payload. Defaults to `any`.\n * @example\n * ```tsx\n * const store = createContextStore(initialState, {\n * incrementBy: ({ set, get }, action: ActionablePayload<number>) => {\n * set((prev) => ({ ...prev, count: get().count + action.payload }));\n * },\n * });\n * ```\n */\nexport type ActionablePayload<Payload = any> = {\n payload: Payload;\n};\n\n/** The state accessors passed as the first argument to every action handler. */\nexport type StateActionProps<Store> = {\n /** Updates the store state. */\n set: (value: SetterArgs<Store>) => void;\n /** Returns the current store state. */\n get: () => Store;\n};\n\n/**\n * A map of action names to action handlers, as passed to {@link createContextStore}.\n * Each handler receives {@link StateActionProps} for reading/writing the\n * store, plus an {@link ActionablePayload} carrying any payload passed by the caller.\n */\nexport type Actions<Store> = {\n [key: string]: (\n stateProps: StateActionProps<Store>,\n action: ActionablePayload\n ) => void | Promise<void>;\n};\n\n/** The object returned by a store's `useStore` hook. */\nexport type UseStoreReturnType<Store, SelectorOutput> = {\n /** The currently selected state. Reading this subscribes the component to updates. */\n state: SelectorOutput;\n /** Returns the currently selected state. Equivalent to reading `state`. */\n get: () => SelectorOutput;\n /** Updates the store state. */\n set: (value: SetterArgs<Store>) => void;\n /** Returns the entire store state, not just the selected slice. */\n selector: () => Store;\n};\n\n/**\n * The object returned by {@link createContextStore}: the `Provider`\n * component plus the hooks and functions used to read, update and\n * subscribe to the store.\n */\nexport type ContextReturnType<Store, A extends Actions<Store>> = {\n /** Wraps the part of the component tree that should have access to the store. */\n Provider: React.FC<React.PropsWithChildren>;\n /**\n * Reads (a selection of) the store state from the nearest `Provider`,\n * re-rendering the component whenever the selected state changes.\n * @param selector - Selects the part of the store the component needs. Defaults to the entire store.\n * @param options - A function used to compare the previous and next selected state. Defaults to the store's `compare` option, or shallow equality.\n * @returns The selected state, plus functions to read and update the store.\n */\n useStore: <SelectorOutput = Store>(\n selector?: (store: Store) => SelectorOutput,\n options?: SelectorOptions<SelectorOutput>\n ) => UseStoreReturnType<Store, SelectorOutput>;\n /**\n * Returns the actions passed to {@link createContextStore}, bound to the\n * current store. Actions declared with an {@link ActionablePayload}\n * parameter take a single payload argument; other actions take none.\n */\n useActions: () => Prettify<ExtractActionKeys<A>>;\n /**\n * Returns a function to update the store state from the nearest\n * `Provider`, without subscribing the component to store updates.\n */\n useSetStore: () => (value: SetterArgs<Store>) => void;\n /**\n * Subscribes to store updates from outside the React component tree.\n * @param selector - Selects the part of the store to watch.\n * @param callback - Called with the selected state whenever it changes.\n * @param options - A function used to compare the previous and next selected state. Defaults to shallow equality.\n * @returns A function to remove the subscription.\n */\n subscribe: <SelectorOutput = Store>(\n selector: (store: Store) => SelectorOutput,\n callback: (state: SelectorOutput) => void,\n options?: SelectorOptions<SelectorOutput>\n ) => () => void;\n /** Removes a subscription created with `subscribe`. */\n unsubscribe: (callback: () => void) => void;\n};\n\nfunction isFunction(value: any): value is (prev: any) => any {\n return typeof value === \"function\";\n}\n\n/**\n * Creates a React context-based store: a `Provider` component plus hooks to\n * read, update and subscribe to its state.\n *\n * Each call creates an independent store with its own React context, so the\n * `Provider` and hooks returned by one call must always be used together.\n * @typeParam Store - The shape of the store state.\n * @typeParam A - The map of named actions available to update the store.\n * @param args - Positional arguments: the `initialState` the store is initialized with, an\n * optional `actions` map of named action handlers used to update the store (see\n * {@link ActionablePayload} for actions that take a payload), and optional `options` to\n * customize the store's behavior.\n * @returns An object with a `Provider` component and hooks to read, update and subscribe to the store's state. See {@link ContextReturnType}.\n * @example\n * ```tsx\n * const store = createContextStore(\n * { count: 0 },\n * {\n * increment: ({ set, get }) => set({ ...get(), count: get().count + 1 }),\n * }\n * );\n *\n * function Counter() {\n * const { state: count } = store.useStore((state) => state.count);\n * const { increment } = store.useActions();\n *\n * return <button onClick={() => increment()}>{count}</button>;\n * }\n *\n * function App() {\n * return (\n * <store.Provider>\n * <Counter />\n * </store.Provider>\n * );\n * }\n * ```\n */\nexport function createContextStore<Store, A extends Actions<Store> = Actions<Store>>(\n ...args: Extract<A, { payload: A }> extends { payload: infer Payload }\n ? [initialState: Store, actions?: A, options?: ContextOptions]\n : [initialState: Store, options?: ContextOptions]\n): ContextReturnType<Store, A>;\nexport function createContextStore<Store, A extends Actions<Store> = Actions<Store>>(\n initialState: Store,\n actions: A = {} as A,\n options: ContextOptions = {}\n): ContextReturnType<Store, A> {\n let globalStore: Store | undefined = options.global\n ? initialState\n : undefined;\n\n function useStoreData({\n defaultSubscriber = [],\n }: {\n defaultSubscriber?: ((state: Store) => void)[];\n } = {}): {\n get: () => Store;\n set: (value: SetterArgs<Store>) => void;\n subscribe: (callback: (state: Store) => void) => () => void;\n } {\n const store = useRef<Store>(globalStore ?? initialState);\n\n const get = useCallback(() => store.current, []);\n\n const subscribers = useRef(\n new Set<(state: Store) => void>(defaultSubscriber)\n );\n\n const set = useCallback((value: SetterArgs<Store>) => {\n store.current = isFunction(value) ? value(store.current) : value;\n\n if (options.global) {\n globalStore = store.current;\n }\n\n subscribers.current.forEach((callback) => callback(store.current));\n }, []);\n\n const subscribe = useCallback((callback: (state: Store) => void) => {\n subscribers.current.add(callback);\n\n return () => subscribers.current.delete(callback);\n }, []);\n\n return {\n get,\n set,\n subscribe,\n };\n }\n\n type UseStoreDataReturnType = ReturnType<typeof useStoreData>;\n\n const StoreContext = createContext<UseStoreDataReturnType | null>(null);\n\n /**\n * Wraps the part of the component tree that should have access to the\n * store. Must wrap any component using `useStore`, `useActions` or `useSetStore`.\n * @remarks\n * If the store was created with `{ global: true }`, state changes made\n * while a `Provider` is mounted are preserved and restored the next time\n * a `Provider` for this store mounts.\n */\n function Provider({ children }: React.PropsWithChildren) {\n return (\n <StoreContext.Provider\n value={useStoreData({ defaultSubscriber: [observable.broadcast] })}\n >\n {children}\n </StoreContext.Provider>\n );\n }\n\n /**\n * This holds all the observables that are subscribed to the store\n * but not being actively used by any component. Helps to broadcast\n * store updates to all the observables that are interested in reacting\n * to state outside the react component tree.\n */\n const observers = new Set<(state: Store) => void>();\n\n /**\n * Subscribes to store updates from outside the React component tree.\n * @param selector - Selects the part of the store to watch. Defaults to the whole store.\n * @param callback - Called with the selected state whenever it changes.\n * @param compare - A function used to compare the previous and next selected state. Defaults to shallow equality.\n * @returns A function to remove the subscription.\n */\n function subscribeExternal<SelectorOutput = Store>(\n selector: (store: Store) => SelectorOutput = (store) =>\n store as unknown as SelectorOutput,\n callback: (state: SelectorOutput) => void,\n compare: SelectorOptions<SelectorOutput> = shallowEqual\n ): () => void {\n let lastSelectedState: SelectorOutput | undefined;\n\n const shouldSendUpdates = (newSelectedState: SelectorOutput) => {\n if (\n lastSelectedState === undefined ||\n !compare(lastSelectedState, newSelectedState)\n ) {\n lastSelectedState = newSelectedState;\n\n return true;\n }\n\n return false;\n };\n\n const selectedCb = (store: Store) => {\n const selected = selector(store);\n const shouldSend = shouldSendUpdates(selected);\n\n if (shouldSend) {\n callback(selector(store));\n }\n };\n observers.add(selectedCb);\n\n return () => observers.delete(selectedCb);\n }\n\n /**\n * A simple observable that proxy store updates to all the subscribers\n * that are interested in reacting to state outside the react component tree.\n */\n const observable = {\n subscribe: subscribeExternal,\n unsubscribe: (callback: (state: Store) => void) => {\n observers.delete(callback);\n },\n broadcast: (state: Store) => {\n observers.forEach((callback) => callback(state));\n },\n } as const;\n\n /**\n * Reads (a selection of) the store state from the nearest `Provider`,\n * re-rendering the component whenever the selected state changes.\n * @param selector - A function that selects the part of the store that is\n * needed by the component. Defaults to the entire store.\n * @param compare - A function used to compare the previous and next\n * selected state; the component only re-renders when it returns `false`.\n * Defaults to the store's `compare` option (see {@link ContextOptions}),\n * or `shallowEqual` from `@pixotope/utils/comparison`.\n * @returns An object containing the selected `state`, a `set` function to\n * update the store, a `get` function equivalent to reading `state`, and a\n * `selector` function to read the entire store without selecting.\n * @example\n * ```tsx\n * const { state, set } = store.useStore((store) => store.user);\n * const { state, set } = store.useStore(\n * (store) => store.user,\n * (first, second) => first.id === second.id\n * );\n * ```\n */\n function useStore<SelectorOutput = Store>(\n selector: (store: Store) => SelectorOutput = (store) =>\n store as unknown as SelectorOutput,\n compare: SelectorOptions<SelectorOutput> = options.compare ?? shallowEqual\n ): UseStoreReturnType<Store, SelectorOutput> {\n const store = useContext(StoreContext);\n const lastSelectedState = useRef<SelectorOutput | undefined>(undefined);\n\n if (!store) {\n throw new Error(\n `[${LIB_NAME}] Store not found. Make sure the component is wrapped in a ${Provider} component.`\n );\n }\n\n const state = useSyncExternalStore(\n store.subscribe,\n () => {\n const selectedState = selector(store.get());\n\n if (\n lastSelectedState.current === undefined ||\n !compare(lastSelectedState.current, selectedState)\n ) {\n lastSelectedState.current = selectedState;\n }\n\n return lastSelectedState.current;\n },\n () => selector(initialState)\n );\n\n return {\n state,\n get: () => state,\n set: store.set,\n selector: () => store.get(),\n };\n }\n\n /**\n * Returns the actions passed to {@link createContextStore}, bound to the\n * current store from the nearest `Provider`. Calling an action does not\n * itself cause a re-render; only the state changes it makes (via `set`)\n * do, and only for components subscribed to the affected state.\n * @returns An object with one bound function per action. Actions declared\n * with an {@link ActionablePayload} parameter take a single payload\n * argument; other actions take no arguments.\n * @example\n * ```tsx\n * const store = createContextStore(initialState, {\n * increment: ({ set, get }) => set({ ...get(), count: get().count + 1 }),\n * incrementBy: ({ set, get }, action: ActionablePayload<number>) =>\n * set({ ...get(), count: get().count + action.payload }),\n * });\n *\n * function Counter() {\n * const { incrementBy } = store.useActions();\n * return <button onClick={() => incrementBy(5)}>+5</button>;\n * }\n * ```\n */\n function useActions() {\n const store = useContext(StoreContext);\n\n if (!store) {\n throw new Error(\n `[${LIB_NAME}] Store not found. Make sure the component is wrapped in a ${Provider} component.`\n );\n }\n\n const actionProxy = useMemo(\n () =>\n new Proxy(actions, {\n get: (target, prop) => {\n const action = target[prop as string];\n\n if (action) {\n return (args: Parameters<typeof action>[\"1\"]) => {\n action(\n {\n set: store.set,\n get: store.get,\n },\n {\n payload: args,\n }\n );\n };\n }\n },\n set: () => {\n throw new Error(`[${LIB_NAME}] Actions cannot be updated`);\n },\n }),\n []\n );\n\n return actionProxy as unknown as ExtractActionKeys<typeof actions>;\n }\n\n /**\n * Returns a function to update the store state from the nearest\n * `Provider`, without subscribing the component to store updates.\n * @returns A function that sets the store state, accepting either the\n * next state directly or an updater function that receives the previous state.\n * @example\n * ```tsx\n * const setStore = store.useSetStore();\n * setStore((prev) => ({ ...prev, count: prev.count + 1 }));\n * ```\n */\n const useSetStore = () => useStore(() => false).set;\n\n return {\n Provider,\n useStore,\n useSetStore,\n useActions,\n subscribe: observable.subscribe,\n unsubscribe: observable.unsubscribe,\n };\n}\n","// src/comparison/shallowEqual.ts\nfunction shallowEqual(objA, objB) {\n if (Object.is(objA, objB)) {\n return true;\n }\n if (typeof objA !== \"object\" || objA === null || typeof objB !== \"object\" || objB === null) {\n return false;\n }\n if (objA instanceof Map && objB instanceof Map) {\n if (objA.size !== objB.size) return false;\n for (const [key, value] of objA) {\n if (!Object.is(value, objB.get(key))) {\n return false;\n }\n }\n return true;\n }\n if (objA instanceof Set && objB instanceof Set) {\n if (objA.size !== objB.size) return false;\n for (const value of objA) {\n if (!objB.has(value)) {\n return false;\n }\n }\n return true;\n }\n const keysA = Object.keys(objA);\n if (keysA.length !== Object.keys(objB).length) {\n return false;\n }\n for (let i = 0; i < keysA.length; i++) {\n if (!Object.prototype.hasOwnProperty.call(objB, keysA[i]) || !Object.is(objA[keysA[i]], objB[keysA[i]])) {\n return false;\n }\n }\n return true;\n}\n\nexport {\n shallowEqual\n};\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixotope/react-context-store",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "A factory for React context-based state stores, with selector hooks, typed actions, and useSyncExternalStore under the hood.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"module": "dist/index.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"tsup": "^8.5.0",
|
|
21
21
|
"typescript": "^5.7.2",
|
|
22
22
|
"vitest": "^3.2.4",
|
|
23
|
-
"@pixotope/utils": "0.
|
|
23
|
+
"@pixotope/utils": "0.4.0"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
26
|
"react": ">=18.0.0"
|