@pixotope/react-context-store 0.0.1
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 +20 -0
- package/.turbo/turbo-check-types.log +4 -0
- package/.turbo/turbo-test.log +14 -0
- package/dist/index.cjs +404 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +54 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +402 -0
- package/dist/index.js.map +1 -0
- package/package.json +33 -0
- package/src/index.ts +1 -0
- package/src/store.test.tsx +238 -0
- package/src/store.tsx +327 -0
- package/src/vitest-setup.ts +1 -0
- package/tsconfig.json +28 -0
- package/vitest.config.mts +21 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import {
|
|
2
|
+
act,
|
|
3
|
+
renderHook,
|
|
4
|
+
RenderHookOptions,
|
|
5
|
+
RenderHookResult,
|
|
6
|
+
} from "@testing-library/react";
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { vi, describe, test, beforeEach, it, expect } from "vitest";
|
|
9
|
+
|
|
10
|
+
import { createContextStore } from "./store";
|
|
11
|
+
|
|
12
|
+
const initialState = {
|
|
13
|
+
count: 0,
|
|
14
|
+
user: {
|
|
15
|
+
name: "John",
|
|
16
|
+
age: 20,
|
|
17
|
+
},
|
|
18
|
+
hobbies: ["football", "basketball"],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type HookFunction<T> = () => T;
|
|
22
|
+
|
|
23
|
+
function createDefaultStore() {
|
|
24
|
+
return createContextStore(initialState);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type ContextRes = ReturnType<typeof createDefaultStore>;
|
|
28
|
+
|
|
29
|
+
const renderHookWithProvider = <T,>(
|
|
30
|
+
hook: HookFunction<T>,
|
|
31
|
+
Provider: ContextRes["Provider"],
|
|
32
|
+
options?: RenderHookOptions<T>
|
|
33
|
+
): RenderHookResult<T, T> => {
|
|
34
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
35
|
+
<Provider>{children}</Provider>
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
return renderHook(hook, { ...options, wrapper });
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
describe("createContextStore", () => {
|
|
42
|
+
let store: ReturnType<typeof createDefaultStore>;
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
store = createContextStore(initialState);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it.concurrent("should return the initial state", async () => {
|
|
49
|
+
const { result } = renderHookWithProvider(
|
|
50
|
+
() => store.useStore(),
|
|
51
|
+
store.Provider
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
expect(result.current.get()).toEqual(initialState);
|
|
55
|
+
});
|
|
56
|
+
it.concurrent("should return the updated state", async () => {
|
|
57
|
+
const { result } = renderHookWithProvider(
|
|
58
|
+
() => store.useStore(),
|
|
59
|
+
store.Provider
|
|
60
|
+
);
|
|
61
|
+
act(() => {
|
|
62
|
+
result.current.set((prev) => ({ ...prev, count: 1 }));
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(result.current.get().count).toEqual(1);
|
|
66
|
+
});
|
|
67
|
+
it.concurrent(
|
|
68
|
+
"should return the reference to the same object if not changed",
|
|
69
|
+
() => {
|
|
70
|
+
const { result } = renderHookWithProvider(
|
|
71
|
+
() => store.useStore(),
|
|
72
|
+
store.Provider
|
|
73
|
+
);
|
|
74
|
+
const prevUser = result.current.get().user;
|
|
75
|
+
act(() => {
|
|
76
|
+
result.current.set((prev) => ({ ...prev, count: 1 }));
|
|
77
|
+
});
|
|
78
|
+
const userAfterCountChange = result.current.get().user;
|
|
79
|
+
|
|
80
|
+
expect(prevUser).toBe(userAfterCountChange);
|
|
81
|
+
|
|
82
|
+
act(() => {
|
|
83
|
+
result.current.set((prev) => ({
|
|
84
|
+
...prev,
|
|
85
|
+
hobbies: ["gardening"],
|
|
86
|
+
}));
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(prevUser).toBe(userAfterCountChange);
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
it.concurrent("should call compare function passed to useStore", () => {
|
|
93
|
+
const compare = vi.fn(() => true);
|
|
94
|
+
const { result } = renderHookWithProvider(
|
|
95
|
+
() => store.useStore((store) => store, compare),
|
|
96
|
+
store.Provider
|
|
97
|
+
);
|
|
98
|
+
act(() => {
|
|
99
|
+
result.current.set((prev) => ({
|
|
100
|
+
...prev,
|
|
101
|
+
user: {
|
|
102
|
+
name: "Micheal",
|
|
103
|
+
age: 30,
|
|
104
|
+
},
|
|
105
|
+
}));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(compare).toBeCalled();
|
|
109
|
+
});
|
|
110
|
+
it.concurrent("should cause re-render if compare returns false", () => {
|
|
111
|
+
const compare = vi.fn(() => false);
|
|
112
|
+
const { result } = renderHookWithProvider(
|
|
113
|
+
() => store.useStore((store) => store, compare),
|
|
114
|
+
store.Provider
|
|
115
|
+
);
|
|
116
|
+
act(() => {
|
|
117
|
+
result.current.set((prev) => ({
|
|
118
|
+
...prev,
|
|
119
|
+
user: {
|
|
120
|
+
name: "Micheal",
|
|
121
|
+
age: 30,
|
|
122
|
+
},
|
|
123
|
+
}));
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
expect(compare).toBeCalled();
|
|
127
|
+
expect(result.current.get().user.name).toEqual("Micheal");
|
|
128
|
+
});
|
|
129
|
+
it.concurrent("should not cause re-render if compare returns true", () => {
|
|
130
|
+
const compare = vi.fn(() => true);
|
|
131
|
+
const { result } = renderHookWithProvider(
|
|
132
|
+
() => store.useStore((store) => store, compare),
|
|
133
|
+
store.Provider
|
|
134
|
+
);
|
|
135
|
+
const prevUser = result.current.get().user;
|
|
136
|
+
act(() => {
|
|
137
|
+
result.current.set((prev) => ({
|
|
138
|
+
...prev,
|
|
139
|
+
user: {
|
|
140
|
+
name: "Micheal",
|
|
141
|
+
age: 30,
|
|
142
|
+
},
|
|
143
|
+
}));
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(result.current.get().user).toEqual(prevUser);
|
|
147
|
+
});
|
|
148
|
+
test.concurrent("selector is called with the current state", () => {
|
|
149
|
+
const selector = vi.fn((state) => state.user);
|
|
150
|
+
renderHookWithProvider(() => store.useStore(selector), store.Provider);
|
|
151
|
+
expect(selector).toBeCalledWith(initialState);
|
|
152
|
+
});
|
|
153
|
+
test.concurrent(
|
|
154
|
+
"selector is called with the current state after update",
|
|
155
|
+
() => {
|
|
156
|
+
const selector = vi.fn((state) => state.user);
|
|
157
|
+
const { result } = renderHookWithProvider(
|
|
158
|
+
() => store.useStore(selector),
|
|
159
|
+
store.Provider
|
|
160
|
+
);
|
|
161
|
+
act(() => {
|
|
162
|
+
result.current.set((prev) => ({
|
|
163
|
+
...prev,
|
|
164
|
+
user: {
|
|
165
|
+
name: "Micheal",
|
|
166
|
+
age: 30,
|
|
167
|
+
},
|
|
168
|
+
}));
|
|
169
|
+
});
|
|
170
|
+
expect(selector).toBeCalledWith({
|
|
171
|
+
...initialState,
|
|
172
|
+
user: {
|
|
173
|
+
name: "Micheal",
|
|
174
|
+
age: 30,
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
test.concurrent("selector passed to userStore returns correct value", () => {
|
|
180
|
+
const selector = vi.fn((state: typeof initialState) => state.user);
|
|
181
|
+
const { result } = renderHookWithProvider(
|
|
182
|
+
() => store.useStore(selector),
|
|
183
|
+
store.Provider
|
|
184
|
+
);
|
|
185
|
+
expect(result.current.get()).toEqual(initialState.user);
|
|
186
|
+
});
|
|
187
|
+
test.concurrent(
|
|
188
|
+
"hook returns a selctor function that returns the correct value",
|
|
189
|
+
() => {
|
|
190
|
+
const { result } = renderHookWithProvider(
|
|
191
|
+
() => store.useStore((state) => state.user),
|
|
192
|
+
store.Provider
|
|
193
|
+
);
|
|
194
|
+
expect(result.current.selector()).toEqual(initialState);
|
|
195
|
+
}
|
|
196
|
+
);
|
|
197
|
+
test.concurrent(
|
|
198
|
+
"hook returns a selctor function that returns the correct value after update",
|
|
199
|
+
() => {
|
|
200
|
+
const { result } = renderHookWithProvider(
|
|
201
|
+
() => store.useStore((state) => state.user),
|
|
202
|
+
store.Provider
|
|
203
|
+
);
|
|
204
|
+
act(() => {
|
|
205
|
+
result.current.set((prev) => ({
|
|
206
|
+
...prev,
|
|
207
|
+
user: {
|
|
208
|
+
name: "Micheal",
|
|
209
|
+
age: 30,
|
|
210
|
+
},
|
|
211
|
+
}));
|
|
212
|
+
});
|
|
213
|
+
expect(result.current.selector()).toEqual({
|
|
214
|
+
...initialState,
|
|
215
|
+
user: {
|
|
216
|
+
name: "Micheal",
|
|
217
|
+
age: 30,
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
);
|
|
222
|
+
it.concurrent("should only cause re-render if selected value changes", () => {
|
|
223
|
+
const selector = vi.fn((state: typeof initialState) => state.user);
|
|
224
|
+
const { result } = renderHookWithProvider(
|
|
225
|
+
() => store.useStore(selector),
|
|
226
|
+
store.Provider
|
|
227
|
+
);
|
|
228
|
+
const prevUser = result.current.get();
|
|
229
|
+
act(() => {
|
|
230
|
+
result.current.set((prev) => ({
|
|
231
|
+
...prev,
|
|
232
|
+
count: 1,
|
|
233
|
+
}));
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
expect(result.current.get()).toBe(prevUser);
|
|
237
|
+
});
|
|
238
|
+
});
|
package/src/store.tsx
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
createContext,
|
|
3
|
+
useCallback,
|
|
4
|
+
useContext,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
} from "react";
|
|
8
|
+
import { useSyncExternalStore } from "use-sync-external-store/shim";
|
|
9
|
+
import { shallowEqual } from "@pixotope/utils/comparison";
|
|
10
|
+
|
|
11
|
+
const LIB_NAME = "@pixotope/react-context-store";
|
|
12
|
+
|
|
13
|
+
export type SelectorOptions<Selected> = (
|
|
14
|
+
first: Selected,
|
|
15
|
+
second: Selected
|
|
16
|
+
) => boolean;
|
|
17
|
+
|
|
18
|
+
export type ContextOptions = {
|
|
19
|
+
/**
|
|
20
|
+
* If true, the store state will be preserved across mounts and un-mounts
|
|
21
|
+
* of the Provider.
|
|
22
|
+
* @default false
|
|
23
|
+
*/
|
|
24
|
+
global?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* A function that will be used to compare the selected state.
|
|
27
|
+
* @default shallowEqual
|
|
28
|
+
*/
|
|
29
|
+
compare?: SelectorOptions<unknown>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type SetterArgs<Store> = Store | ((prev: Store) => Store);
|
|
33
|
+
type ExtractActionKeys<T> = {
|
|
34
|
+
[K in keyof T]: T[K] extends (
|
|
35
|
+
stateProps: never,
|
|
36
|
+
action: infer A
|
|
37
|
+
) => void | Promise<void>
|
|
38
|
+
? A extends ActionablePayload<infer Payload>
|
|
39
|
+
? Payload extends undefined
|
|
40
|
+
? () => void
|
|
41
|
+
: (payload: Payload) => void
|
|
42
|
+
: () => void
|
|
43
|
+
: never;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type Prettify<T> = {
|
|
47
|
+
[K in keyof T]: T[K];
|
|
48
|
+
} & {};
|
|
49
|
+
|
|
50
|
+
export type ActionablePayload<Payload = any> = {
|
|
51
|
+
payload: Payload;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
type StateActionProps<Store> = {
|
|
55
|
+
set: (value: SetterArgs<Store>) => void;
|
|
56
|
+
get: () => Store;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
type Actions<Store> = {
|
|
60
|
+
[key: string]: (
|
|
61
|
+
stateProps: StateActionProps<Store>,
|
|
62
|
+
action: ActionablePayload
|
|
63
|
+
) => void | Promise<void>;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
type ContextReturnType<Store, A extends Actions<Store>> = {
|
|
67
|
+
Provider: React.FC<{ children: React.ReactNode }>;
|
|
68
|
+
useStore: <SelectorOutput = Store>(
|
|
69
|
+
selector?: (store: Store) => SelectorOutput,
|
|
70
|
+
options?: SelectorOptions<SelectorOutput>
|
|
71
|
+
) => {
|
|
72
|
+
get: () => SelectorOutput;
|
|
73
|
+
set: (value: SetterArgs<Store>) => void;
|
|
74
|
+
selector: () => Store;
|
|
75
|
+
};
|
|
76
|
+
useActions: () => Prettify<ExtractActionKeys<A>>;
|
|
77
|
+
useSetStore: () => (value: SetterArgs<Store>) => void;
|
|
78
|
+
subscribe: <SelectorOutput = Store>(
|
|
79
|
+
selector: (store: Store) => SelectorOutput,
|
|
80
|
+
callback: (state: SelectorOutput) => void,
|
|
81
|
+
options?: SelectorOptions<SelectorOutput>
|
|
82
|
+
) => () => void;
|
|
83
|
+
unsubscribe: (callback: () => void) => void;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
function isFunction(value: any): value is (prev: any) => any {
|
|
87
|
+
return typeof value === "function";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function createContextStore<Store, A extends Actions<Store>>(
|
|
91
|
+
...args: Extract<A, { payload: A }> extends { payload: infer Payload }
|
|
92
|
+
? [initialState: Store, actions?: A, options?: ContextOptions]
|
|
93
|
+
: [initialState: Store, options?: ContextOptions]
|
|
94
|
+
): ContextReturnType<Store, A>;
|
|
95
|
+
export function createContextStore<Store, A extends Actions<Store>>(
|
|
96
|
+
initialState: Store,
|
|
97
|
+
actions: A = {} as A,
|
|
98
|
+
options: ContextOptions = {}
|
|
99
|
+
): ContextReturnType<Store, A> {
|
|
100
|
+
let globalStore: Store | undefined = options.global
|
|
101
|
+
? initialState
|
|
102
|
+
: undefined;
|
|
103
|
+
|
|
104
|
+
function useStoreData({
|
|
105
|
+
defaultSubscriber = [],
|
|
106
|
+
}: {
|
|
107
|
+
defaultSubscriber?: ((state: Store) => void)[];
|
|
108
|
+
} = {}): {
|
|
109
|
+
get: () => Store;
|
|
110
|
+
set: (value: SetterArgs<Store>) => void;
|
|
111
|
+
subscribe: (callback: (state: Store) => void) => () => void;
|
|
112
|
+
} {
|
|
113
|
+
const store = useRef<Store>(globalStore ?? initialState);
|
|
114
|
+
|
|
115
|
+
const get = useCallback(() => store.current, []);
|
|
116
|
+
|
|
117
|
+
const subscribers = useRef(
|
|
118
|
+
new Set<(state: Store) => void>(defaultSubscriber)
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const set = useCallback((value: SetterArgs<Store>) => {
|
|
122
|
+
store.current = isFunction(value) ? value(store.current) : value;
|
|
123
|
+
|
|
124
|
+
if (options.global) {
|
|
125
|
+
globalStore = store.current;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
subscribers.current.forEach((callback) => callback(store.current));
|
|
129
|
+
}, []);
|
|
130
|
+
|
|
131
|
+
const subscribe = useCallback((callback: (state: Store) => void) => {
|
|
132
|
+
subscribers.current.add(callback);
|
|
133
|
+
|
|
134
|
+
return () => subscribers.current.delete(callback);
|
|
135
|
+
}, []);
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
get,
|
|
139
|
+
set,
|
|
140
|
+
subscribe,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
type UseStoreDataReturnType = ReturnType<typeof useStoreData>;
|
|
145
|
+
|
|
146
|
+
const StoreContext = createContext<UseStoreDataReturnType | null>(null);
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* This is the provider that will be used to wrap the react component tree
|
|
150
|
+
* to provide the store to all the components in the tree.
|
|
151
|
+
*/
|
|
152
|
+
function Provider({ children }: { children: React.ReactNode }) {
|
|
153
|
+
return (
|
|
154
|
+
<StoreContext.Provider
|
|
155
|
+
value={useStoreData({ defaultSubscriber: [observable.broadcast] })}
|
|
156
|
+
>
|
|
157
|
+
{children}
|
|
158
|
+
</StoreContext.Provider>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* This holds all the observables that are subscribed to the store
|
|
164
|
+
* but not being actively used by any component. Helps to broadcast
|
|
165
|
+
* store updates to all the observables that are interested in reacting
|
|
166
|
+
* to state outside the react component tree.
|
|
167
|
+
*/
|
|
168
|
+
const observers = new Set<(state: Store) => void>();
|
|
169
|
+
|
|
170
|
+
function subscribeExternal<SelectorOutput = Store>(
|
|
171
|
+
selector: (store: Store) => SelectorOutput = (store) =>
|
|
172
|
+
store as unknown as SelectorOutput,
|
|
173
|
+
callback: (state: SelectorOutput) => void,
|
|
174
|
+
compare: SelectorOptions<SelectorOutput> = (first, second) =>
|
|
175
|
+
first === second
|
|
176
|
+
): () => void {
|
|
177
|
+
let lastSelectedState: SelectorOutput | undefined;
|
|
178
|
+
|
|
179
|
+
const shouldSendUpdates = (newSelectedState: SelectorOutput) => {
|
|
180
|
+
if (
|
|
181
|
+
lastSelectedState === undefined ||
|
|
182
|
+
!compare(lastSelectedState, newSelectedState)
|
|
183
|
+
) {
|
|
184
|
+
lastSelectedState = newSelectedState;
|
|
185
|
+
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return false;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const selectedCb = (store: Store) => {
|
|
193
|
+
const selected = selector(store);
|
|
194
|
+
const shouldSend = shouldSendUpdates(selected);
|
|
195
|
+
|
|
196
|
+
if (shouldSend) {
|
|
197
|
+
callback(selector(store));
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
observers.add(selectedCb);
|
|
201
|
+
|
|
202
|
+
return () => observers.delete(selectedCb);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* A simple observable that proxy store updates to all the subscribers
|
|
207
|
+
* that are interested in reacting to state outside the react component tree.
|
|
208
|
+
*/
|
|
209
|
+
const observable = {
|
|
210
|
+
subscribe: subscribeExternal,
|
|
211
|
+
unsubscribe: (callback: (state: Store) => void) => {
|
|
212
|
+
observers.delete(callback);
|
|
213
|
+
},
|
|
214
|
+
broadcast: (state: Store) => {
|
|
215
|
+
observers.forEach((callback) => callback(state));
|
|
216
|
+
},
|
|
217
|
+
} as const;
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* This is the hook that will be used to access the store from any component
|
|
221
|
+
* in the react component tree.
|
|
222
|
+
* @param selector A function that will be used to select the part of the store
|
|
223
|
+
* that is needed by the component.
|
|
224
|
+
* @param options Options to customize the behavior of the hook.
|
|
225
|
+
* @returns An object with the selected state, a function to update the store
|
|
226
|
+
* and a function to get the entire store.
|
|
227
|
+
* @example
|
|
228
|
+
* ```tsx
|
|
229
|
+
* const { get, set } = useStore(store => store.user);
|
|
230
|
+
* const { get, set } = useStore(store => store.user, { deepEqual: false });
|
|
231
|
+
* const { get, set } = useStore(store => store.user, {
|
|
232
|
+
* compare: (first, second) => first.id === second.id
|
|
233
|
+
* });
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
236
|
+
function useStore<SelectorOutput = Store>(
|
|
237
|
+
selector: (store: Store) => SelectorOutput = (store) =>
|
|
238
|
+
store as unknown as SelectorOutput,
|
|
239
|
+
compare: SelectorOptions<SelectorOutput> = options.compare ?? shallowEqual
|
|
240
|
+
): {
|
|
241
|
+
get: () => SelectorOutput;
|
|
242
|
+
set: (value: SetterArgs<Store>) => void;
|
|
243
|
+
selector: () => Store;
|
|
244
|
+
} {
|
|
245
|
+
const store = useContext(StoreContext);
|
|
246
|
+
const lastSelectedState = useRef<SelectorOutput | undefined>(undefined);
|
|
247
|
+
|
|
248
|
+
if (!store) {
|
|
249
|
+
throw new Error(
|
|
250
|
+
`[${LIB_NAME}] Store not found. Make sure the component is wrapped in a ${Provider} component.`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const state = useSyncExternalStore(
|
|
255
|
+
store.subscribe,
|
|
256
|
+
() => {
|
|
257
|
+
const selectedState = selector(store.get());
|
|
258
|
+
|
|
259
|
+
if (
|
|
260
|
+
lastSelectedState.current === undefined ||
|
|
261
|
+
!compare(lastSelectedState.current, selectedState)
|
|
262
|
+
) {
|
|
263
|
+
lastSelectedState.current = selectedState;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return lastSelectedState.current;
|
|
267
|
+
},
|
|
268
|
+
() => selector(initialState)
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
get: () => state,
|
|
273
|
+
set: store.set,
|
|
274
|
+
selector: () => store.get(),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function useActions() {
|
|
279
|
+
const store = useContext(StoreContext);
|
|
280
|
+
|
|
281
|
+
if (!store) {
|
|
282
|
+
throw new Error(
|
|
283
|
+
`[${LIB_NAME}] Store not found. Make sure the component is wrapped in a ${Provider} component.`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const actionProxy = useMemo(
|
|
288
|
+
() =>
|
|
289
|
+
new Proxy(actions, {
|
|
290
|
+
get: (target, prop) => {
|
|
291
|
+
const action = target[prop as string];
|
|
292
|
+
|
|
293
|
+
if (action) {
|
|
294
|
+
return (args: Parameters<typeof action>["1"]) => {
|
|
295
|
+
action(
|
|
296
|
+
{
|
|
297
|
+
set: store.set,
|
|
298
|
+
get: store.get,
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
payload: args,
|
|
302
|
+
}
|
|
303
|
+
);
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
set: () => {
|
|
308
|
+
throw new Error(`[${LIB_NAME}] Actions cannot be updated`);
|
|
309
|
+
},
|
|
310
|
+
}),
|
|
311
|
+
[]
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
return actionProxy as unknown as ExtractActionKeys<typeof actions>;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const useSetStore = () => useStore(() => false).set;
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
Provider,
|
|
321
|
+
useStore,
|
|
322
|
+
useSetStore,
|
|
323
|
+
useActions,
|
|
324
|
+
subscribe: observable.subscribe,
|
|
325
|
+
unsubscribe: observable.unsubscribe,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"baseUrl": "./src",
|
|
4
|
+
"rootDir": "./src",
|
|
5
|
+
"target": "es6",
|
|
6
|
+
"module": "es6",
|
|
7
|
+
"lib": ["es6", "dom", "dom.iterable"],
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
"allowSyntheticDefaultImports": true,
|
|
11
|
+
"useDefineForClassFields": true,
|
|
12
|
+
"strict": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"sourceMap": true,
|
|
15
|
+
"resolveJsonModule": true,
|
|
16
|
+
"isolatedModules": true,
|
|
17
|
+
"esModuleInterop": true,
|
|
18
|
+
"noEmit": true,
|
|
19
|
+
"noUnusedLocals": false,
|
|
20
|
+
"noUnusedParameters": false,
|
|
21
|
+
"noImplicitReturns": false,
|
|
22
|
+
"forceConsistentCasingInFileNames": true,
|
|
23
|
+
"jsx": "react",
|
|
24
|
+
"types": ["vitest/globals"]
|
|
25
|
+
},
|
|
26
|
+
"include": ["src"],
|
|
27
|
+
"exclude": ["node_modules"]
|
|
28
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
globals: true,
|
|
6
|
+
environment: "jsdom",
|
|
7
|
+
setupFiles: "./src/vitest-setup.ts",
|
|
8
|
+
testTimeout: 30_000,
|
|
9
|
+
hookTimeout: 30_000,
|
|
10
|
+
css: true,
|
|
11
|
+
include: ["**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
|
|
12
|
+
server: {
|
|
13
|
+
deps: {
|
|
14
|
+
inline: [],
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
resolve: {
|
|
19
|
+
alias: {},
|
|
20
|
+
},
|
|
21
|
+
});
|