react-state-custom 1.0.24 → 1.0.25
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/LICENSE +21 -0
- package/README.md +17 -1
- package/dist/index.es.js +282 -294
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/index.umd.js.map +1 -1
- package/dist/state-utils/createAutoCtx.d.ts +1 -1
- package/dist/state-utils/createRootCtx.d.ts +1 -0
- package/dist/state-utils/ctx.d.ts +1 -0
- package/package.json +18 -4
- package/.github/copilot-instructions.md +0 -57
- package/.github/workflows/deploy.yml +0 -56
- package/.vscode/extensions.json +0 -5
- package/.vscode/settings.json +0 -8
- package/API_DOCUMENTATION.md +0 -1012
- package/dist/dev.d.ts +0 -0
- package/dist/examples/Playground.d.ts +0 -1
- package/dist/examples/cart/app.d.ts +0 -1
- package/dist/examples/cart/index.d.ts +0 -3
- package/dist/examples/cart/state.d.ts +0 -23
- package/dist/examples/cart/view.d.ts +0 -4
- package/dist/examples/counter/app.d.ts +0 -1
- package/dist/examples/counter/index.d.ts +0 -2
- package/dist/examples/counter/state.d.ts +0 -6
- package/dist/examples/counter/view.d.ts +0 -2
- package/dist/examples/form/app.d.ts +0 -1
- package/dist/examples/form/index.d.ts +0 -3
- package/dist/examples/form/state.d.ts +0 -16
- package/dist/examples/form/view.d.ts +0 -4
- package/dist/examples/timer/app.d.ts +0 -1
- package/dist/examples/timer/index.d.ts +0 -2
- package/dist/examples/timer/state.d.ts +0 -11
- package/dist/examples/timer/view.d.ts +0 -4
- package/dist/examples/todo/app.d.ts +0 -1
- package/dist/examples/todo/index.d.ts +0 -3
- package/dist/examples/todo/state.d.ts +0 -17
- package/dist/examples/todo/view.d.ts +0 -4
- package/fix-vscode-yarn-pnp.sh +0 -26
- package/index.html +0 -14
- package/src/dev-tool/DataViewComponent.tsx +0 -17
- package/src/dev-tool/DevTool.css +0 -134
- package/src/dev-tool/DevTool.tsx +0 -20
- package/src/dev-tool/DevToolState.tsx +0 -78
- package/src/dev-tool/StateLabelRender.tsx +0 -38
- package/src/dev-tool/useHighlight.tsx +0 -56
- package/src/dev.tsx +0 -7
- package/src/examples/Playground.tsx +0 -180
- package/src/examples/cart/app.tsx +0 -16
- package/src/examples/cart/index.ts +0 -3
- package/src/examples/cart/state.ts +0 -67
- package/src/examples/cart/view.tsx +0 -62
- package/src/examples/counter/app.tsx +0 -14
- package/src/examples/counter/index.ts +0 -2
- package/src/examples/counter/state.ts +0 -22
- package/src/examples/counter/state.tsx?raw +0 -0
- package/src/examples/counter/view.tsx +0 -20
- package/src/examples/form/app.tsx +0 -16
- package/src/examples/form/index.ts +0 -3
- package/src/examples/form/state.ts +0 -58
- package/src/examples/form/view.tsx +0 -53
- package/src/examples/timer/app.tsx +0 -16
- package/src/examples/timer/index.ts +0 -2
- package/src/examples/timer/state.ts +0 -43
- package/src/examples/timer/view.tsx +0 -26
- package/src/examples/todo/app.tsx +0 -16
- package/src/examples/todo/index.ts +0 -3
- package/src/examples/todo/state.ts +0 -54
- package/src/examples/todo/view.tsx +0 -47
- package/src/index.ts +0 -22
- package/src/state-utils/createAutoCtx.tsx +0 -191
- package/src/state-utils/createRootCtx.tsx +0 -117
- package/src/state-utils/ctx.ts +0 -346
- package/src/state-utils/useArrayHash.ts +0 -53
- package/src/state-utils/useObjectHash.ts +0 -53
- package/src/state-utils/useQuickSubscribe.ts +0 -110
- package/src/state-utils/useRefValue.ts +0 -8
- package/src/state-utils/utils.ts +0 -43
- package/src/vite-env.d.ts +0 -6
- package/tsconfig.json +0 -27
- package/vite.config.dev.ts +0 -16
- package/vite.config.ts +0 -39
package/src/state-utils/ctx.ts
DELETED
|
@@ -1,346 +0,0 @@
|
|
|
1
|
-
import { debounce, memoize } from "./utils";
|
|
2
|
-
import { useEffect, useMemo, useState } from "react"
|
|
3
|
-
import { useArrayHash } from "./useArrayHash"
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const CHANGE_EVENT = "@--change-event"
|
|
8
|
-
|
|
9
|
-
class DataEvent<D> extends Event {
|
|
10
|
-
constructor(
|
|
11
|
-
public event: keyof D,
|
|
12
|
-
public value: D[typeof event] | undefined
|
|
13
|
-
) {
|
|
14
|
-
super(String(event));
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
class ChangeEvent<D> extends Event {
|
|
19
|
-
constructor(
|
|
20
|
-
public value: DataEvent<D>
|
|
21
|
-
) {
|
|
22
|
-
super(CHANGE_EVENT, value);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Generic context for managing shared state and event subscriptions.
|
|
28
|
-
* @template D - The shape of the data managed by the context.
|
|
29
|
-
*/
|
|
30
|
-
export class Context<D> extends EventTarget {
|
|
31
|
-
/**
|
|
32
|
-
* Create a new Context instance.
|
|
33
|
-
* @param name - The name of the context (for debugging).
|
|
34
|
-
*/
|
|
35
|
-
constructor(public name: string) {
|
|
36
|
-
console.log("[CONTEXT] %s", name)
|
|
37
|
-
// this.event.setMaxListeners(100)
|
|
38
|
-
super();
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* The current data held by the context.
|
|
43
|
-
*/
|
|
44
|
-
public data: Partial<D> = {}
|
|
45
|
-
/**
|
|
46
|
-
* Registry for tracking active keys (for duplicate detection).
|
|
47
|
-
*/
|
|
48
|
-
public registry = new Set<string>()
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Publish a value to the context and notify subscribers if it changed.
|
|
52
|
-
* @param key - The key to update.
|
|
53
|
-
* @param value - The new value.
|
|
54
|
-
*/
|
|
55
|
-
public publish(key: keyof D, value: D[typeof key] | undefined) {
|
|
56
|
-
|
|
57
|
-
if (value != this.data[key]) {
|
|
58
|
-
this.data[key] = value
|
|
59
|
-
let event = new DataEvent(key, value);
|
|
60
|
-
this.dispatchEvent(event);
|
|
61
|
-
this.dispatchEvent(new ChangeEvent(event))
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Subscribe to changes for a specific key in the context.
|
|
67
|
-
* @param key - The key to subscribe to.
|
|
68
|
-
* @param _listener - Callback invoked with the new value.
|
|
69
|
-
* @returns Unsubscribe function.
|
|
70
|
-
*/
|
|
71
|
-
public subscribe(key: keyof D, _listener: (e: D[typeof key] | undefined) => void) {
|
|
72
|
-
|
|
73
|
-
const listener = ({ event, value }: any) => {
|
|
74
|
-
_listener(value)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
this.addEventListener(String(key), listener)
|
|
78
|
-
// console.log("listenerCount:", String(key), this.event.listenerCount(String(key)))
|
|
79
|
-
|
|
80
|
-
if (key in this.data) _listener(this.data[key])
|
|
81
|
-
|
|
82
|
-
return () => this.removeEventListener(String(key), listener)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
public subscribeAll(_listener: (changeKey: keyof D, newData: Partial<D>) => void) {
|
|
86
|
-
|
|
87
|
-
const listener = (event: any) => {
|
|
88
|
-
if (event instanceof ChangeEvent) {
|
|
89
|
-
const { value: data } = event
|
|
90
|
-
_listener(data.event as any as keyof D, this.data)
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
this.addEventListener(String(CHANGE_EVENT), listener)
|
|
95
|
-
|
|
96
|
-
return () => this.removeEventListener(String(CHANGE_EVENT), listener)
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Get or create a memoized Context instance by name.
|
|
104
|
-
* @param name - The context name.
|
|
105
|
-
* @returns The Context instance.
|
|
106
|
-
*/
|
|
107
|
-
export const getContext = memoize((name: string) => new Context<any>(name))
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Type alias for a function that returns a Context instance.
|
|
111
|
-
*/
|
|
112
|
-
export type getContext<D> = (e: string) => Context<D>
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* React hook to get a typed Context instance by name.
|
|
116
|
-
* @param name - The context name.
|
|
117
|
-
* @returns The Context instance.
|
|
118
|
-
*/
|
|
119
|
-
export const useDataContext = <D>(name: string = "noname") => {
|
|
120
|
-
|
|
121
|
-
const ctx = useMemo(() => getContext(name), [name])
|
|
122
|
-
|
|
123
|
-
return ctx as any as Context<D>
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Internal hook to check for duplicate registry entries in a context.
|
|
128
|
-
* Warns if any of the provided names are already registered.
|
|
129
|
-
* @param ctx - The context instance.
|
|
130
|
-
* @param names - Names to check and register.
|
|
131
|
-
*/
|
|
132
|
-
const useRegistryChecker = (ctx: Context<any> | undefined, ...names: string[]) => {
|
|
133
|
-
// return;
|
|
134
|
-
const stack = new Error("[ctx] useRegistryChecker failed " + JSON.stringify({ names, ctx: ctx?.name ?? 'undefined' }))
|
|
135
|
-
|
|
136
|
-
useEffect(
|
|
137
|
-
() => {
|
|
138
|
-
if (ctx) {
|
|
139
|
-
if (names.some(name => ctx.registry.has(name))) {
|
|
140
|
-
console.error(stack)
|
|
141
|
-
}
|
|
142
|
-
names.forEach(e => ctx.registry.add(e))
|
|
143
|
-
|
|
144
|
-
// console.debug("[ctx] %s%s add datasource", componentId, ctx.name, names)
|
|
145
|
-
return () => {
|
|
146
|
-
// console.debug("[ctx] %s %s remove datasource", componentId, ctx.name, names)
|
|
147
|
-
|
|
148
|
-
names.forEach(e => ctx.registry.delete(e))
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
},
|
|
152
|
-
[ctx, names.length]
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* React hook to publish a value to the context when it changes.
|
|
159
|
-
* @param ctx - The context instance.
|
|
160
|
-
* @param key - The key to update.
|
|
161
|
-
* @param value - The new value.
|
|
162
|
-
*/
|
|
163
|
-
export const useDataSource = <D, K extends keyof D>(ctx: Context<D> | undefined, key: K, value: D[K] | undefined) => {
|
|
164
|
-
//@ts-check
|
|
165
|
-
useEffect(() => {
|
|
166
|
-
if (ctx && ctx.data[key] != value) {
|
|
167
|
-
|
|
168
|
-
ctx.publish(key, value)
|
|
169
|
-
}
|
|
170
|
-
}, [key, value, ctx])
|
|
171
|
-
|
|
172
|
-
useRegistryChecker(ctx, key as any)
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* React hook to subscribe to a context value, with optional debounce.
|
|
177
|
-
* @param ctx - The context instance.
|
|
178
|
-
* @param key - The key to subscribe to.
|
|
179
|
-
* @param debounceTime - Debounce time in ms (default 0).
|
|
180
|
-
* @returns The current value for the key.
|
|
181
|
-
*/
|
|
182
|
-
export const useDataSubscribe = <D, K extends keyof D>(ctx: Context<D> | undefined, key: K, debounceTime = 0): D[K] | undefined => {
|
|
183
|
-
//@ts-check
|
|
184
|
-
const [{ value }, setState] = useState(() => ({ value: ctx?.data?.[key] }))
|
|
185
|
-
|
|
186
|
-
useEffect(() => {
|
|
187
|
-
if (ctx) {
|
|
188
|
-
let callback = debounceTime == 0
|
|
189
|
-
? (value: any) => setState({ value } as any)
|
|
190
|
-
: debounce((value: any) => setState({ value } as any), debounceTime)
|
|
191
|
-
let unsub = ctx.subscribe(key, callback)
|
|
192
|
-
value != ctx.data[key] && setState({ value: ctx.data[key] })
|
|
193
|
-
return () => {
|
|
194
|
-
unsub()
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}, [key, ctx])
|
|
198
|
-
|
|
199
|
-
return ctx?.data[key]
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* React hook to subscribe to a context value and transform it before returning.
|
|
204
|
-
* @param ctx - The context instance.
|
|
205
|
-
* @param key - The key to subscribe to.
|
|
206
|
-
* @param transform - Function to transform the value.
|
|
207
|
-
* @returns The transformed value.
|
|
208
|
-
*/
|
|
209
|
-
export const useDataSubscribeWithTransform = <D, K extends keyof D, E>(ctx: Context<D> | undefined, key: K, transform: (e: D[K] | undefined) => E): E => {
|
|
210
|
-
const [, setState] = useState(0)
|
|
211
|
-
const result = useMemo(
|
|
212
|
-
() => transform(ctx?.data[key]),
|
|
213
|
-
[transform, ctx?.data[key]]
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
useEffect(() => {
|
|
217
|
-
if (ctx) {
|
|
218
|
-
let preValue = result
|
|
219
|
-
let callback = () => {
|
|
220
|
-
let newValue = transform(ctx.data[key])
|
|
221
|
-
if (newValue != preValue) {
|
|
222
|
-
preValue = newValue;
|
|
223
|
-
setState(e => e + 1)
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
let unsub = ctx.subscribe(key, callback)
|
|
227
|
-
callback();
|
|
228
|
-
return () => unsub()
|
|
229
|
-
}
|
|
230
|
-
}, [key, ctx])
|
|
231
|
-
|
|
232
|
-
return result
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* React hook to publish multiple values to the context.
|
|
237
|
-
* @param ctx - The context instance.
|
|
238
|
-
* @param entries - Array of [key, value] pairs to update.
|
|
239
|
-
*/
|
|
240
|
-
export const useDataSourceMultiple = <D, T extends readonly (keyof D)[]>(
|
|
241
|
-
ctx: Context<D> | undefined,
|
|
242
|
-
...entries: { -readonly [P in keyof T]: [T[P], D[T[P]]] }
|
|
243
|
-
) => {
|
|
244
|
-
//@ts-check
|
|
245
|
-
useEffect(() => {
|
|
246
|
-
if (ctx) {
|
|
247
|
-
for (let [key, value] of entries) {
|
|
248
|
-
ctx.data[key] != value && ctx.publish(key, value)
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}, [ctx, useArrayHash(entries.flat())])
|
|
252
|
-
|
|
253
|
-
useRegistryChecker(ctx, ...entries.map(e => e[0]) as any)
|
|
254
|
-
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* React hook to subscribe to multiple context values.
|
|
259
|
-
* @param ctx - The context instance.
|
|
260
|
-
* @param keys - Keys to subscribe to.
|
|
261
|
-
* @returns An object with the current values for the keys.
|
|
262
|
-
*/
|
|
263
|
-
export const useDataSubscribeMultiple = <D, K extends (keyof D)[]>(
|
|
264
|
-
ctx: Context<D> | undefined,
|
|
265
|
-
...keys: K
|
|
266
|
-
): { [i in keyof K]: D[K[i]] | undefined } => {
|
|
267
|
-
const [, setCounter] = useState(0)
|
|
268
|
-
|
|
269
|
-
const returnValues = keys.map(key => ctx?.data?.[key])
|
|
270
|
-
|
|
271
|
-
useEffect(() => {
|
|
272
|
-
if (ctx) {
|
|
273
|
-
let prevValues = returnValues
|
|
274
|
-
const callback = debounce(() => {
|
|
275
|
-
let currentValues = keys.map(key => ctx?.data?.[key])
|
|
276
|
-
if (keys.some((key, i) => prevValues[i] != currentValues[i])) {
|
|
277
|
-
// console.log("DIFF", keys.filter((e, i) => prevValues[i] != currentValues[i]))
|
|
278
|
-
prevValues = currentValues
|
|
279
|
-
setCounter(c => c + 1)
|
|
280
|
-
}
|
|
281
|
-
}, 1)
|
|
282
|
-
|
|
283
|
-
let handles = keys.map(key => ctx.subscribe(key, callback))
|
|
284
|
-
|
|
285
|
-
let firstCall = setTimeout(callback, 1);
|
|
286
|
-
|
|
287
|
-
return () => {
|
|
288
|
-
clearTimeout(firstCall)
|
|
289
|
-
callback.cancel();
|
|
290
|
-
handles.forEach(unsub => unsub())
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
}
|
|
294
|
-
}, [ctx, ...keys])
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
return Object
|
|
298
|
-
.fromEntries(keys.map((key, index) => [key, returnValues[index]])) as any
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* React hook to subscribe to multiple context values with throttling.
|
|
303
|
-
* @param ctx - The context instance.
|
|
304
|
-
* @param debounceTime - Debounce time in ms (default 50).
|
|
305
|
-
* @param keys - Keys to subscribe to.
|
|
306
|
-
* @returns Array of current values for the keys.
|
|
307
|
-
*/
|
|
308
|
-
export const useDataSubscribeMultipleWithDebounce = <D, K extends (keyof D)[]>(
|
|
309
|
-
ctx: Context<D> | undefined,
|
|
310
|
-
debounceTime = 50,
|
|
311
|
-
...keys: K
|
|
312
|
-
): { [i in keyof K]: D[K[i]] | undefined } => {
|
|
313
|
-
//@ts-check
|
|
314
|
-
const [, setCounter] = useState(0)
|
|
315
|
-
|
|
316
|
-
const returnValues = keys.map(key => ctx?.data?.[key])
|
|
317
|
-
|
|
318
|
-
useEffect(() => {
|
|
319
|
-
if (ctx) {
|
|
320
|
-
let prevValues = returnValues
|
|
321
|
-
const callback = debounce(() => {
|
|
322
|
-
let currentValues = keys.map(key => ctx?.data?.[key])
|
|
323
|
-
if (keys.some((key, i) => prevValues[i] != currentValues[i])) {
|
|
324
|
-
prevValues = currentValues
|
|
325
|
-
setCounter(c => c + 1)
|
|
326
|
-
}
|
|
327
|
-
}, debounceTime)
|
|
328
|
-
|
|
329
|
-
let handles = keys.map(key => ctx.subscribe(key, callback))
|
|
330
|
-
|
|
331
|
-
let firstCall = setTimeout(callback, 1);
|
|
332
|
-
|
|
333
|
-
return () => {
|
|
334
|
-
clearTimeout(firstCall)
|
|
335
|
-
callback.cancel();
|
|
336
|
-
handles.forEach(unsub => unsub())
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
}
|
|
340
|
-
}, [ctx, ...keys])
|
|
341
|
-
|
|
342
|
-
return returnValues as any
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { useRef } from "react"
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const randomHash = () => Math.random().toString().slice(2)
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* useArrayHash
|
|
8
|
-
*
|
|
9
|
-
* A custom hook that computes a stable hash for an array of values.
|
|
10
|
-
* The hash changes only when the array's contents differ from the previous call.
|
|
11
|
-
*
|
|
12
|
-
* @param e - The input array to hash.
|
|
13
|
-
* @returns A string hash that updates when the array changes.
|
|
14
|
-
*
|
|
15
|
-
* How it works:
|
|
16
|
-
* - Tracks the previous array and its hash using a `useRef`.
|
|
17
|
-
* - Compares the new array to the previous one by length and element equality.
|
|
18
|
-
* - If any difference is detected, generates a new random hash.
|
|
19
|
-
*/
|
|
20
|
-
export const useArrayHash = (e: any[]): string => {
|
|
21
|
-
|
|
22
|
-
const { current: { computedHash } } = useRef({
|
|
23
|
-
/**
|
|
24
|
-
* Getter for the computed hash function.
|
|
25
|
-
*
|
|
26
|
-
* - Initializes with an empty array and a random hash.
|
|
27
|
-
* - Returns a function that compares the current array to the previous one.
|
|
28
|
-
* - Updates the hash if any difference is detected.
|
|
29
|
-
*/
|
|
30
|
-
get computedHash() {
|
|
31
|
-
let currentValues: any[] = []
|
|
32
|
-
let currentHash = randomHash()
|
|
33
|
-
return (e: any[]) => {
|
|
34
|
-
let isDiff = false
|
|
35
|
-
|
|
36
|
-
// Check for differences in array existence, length, or elements.
|
|
37
|
-
isDiff = isDiff || ((!e) != (!currentValues))
|
|
38
|
-
isDiff = isDiff || (e?.length != currentValues?.length);
|
|
39
|
-
isDiff = isDiff || (e.some((f, i) => f != currentValues[i]));
|
|
40
|
-
|
|
41
|
-
// Update the hash if differences are found.
|
|
42
|
-
currentValues = e;
|
|
43
|
-
if (isDiff) {
|
|
44
|
-
currentHash = randomHash()
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return currentHash
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
return computedHash(e)
|
|
53
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { useRef } from "react"
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const randomHash = () => Math.random().toString().slice(2)
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* useArrayHash
|
|
8
|
-
*
|
|
9
|
-
* A custom hook that computes a stable hash for an array of values.
|
|
10
|
-
* The hash changes only when the array's contents differ from the previous call.
|
|
11
|
-
*
|
|
12
|
-
* @param e - The input array to hash.
|
|
13
|
-
* @returns A string hash that updates when the array changes.
|
|
14
|
-
*
|
|
15
|
-
* How it works:
|
|
16
|
-
* - Tracks the previous array and its hash using a `useRef`.
|
|
17
|
-
* - Compares the new array to the previous one by length and element equality.
|
|
18
|
-
* - If any difference is detected, generates a new random hash.
|
|
19
|
-
*/
|
|
20
|
-
export const useArrayHash = (e: any[]): string => {
|
|
21
|
-
|
|
22
|
-
const { current: { computedHash } } = useRef({
|
|
23
|
-
/**
|
|
24
|
-
* Getter for the computed hash function.
|
|
25
|
-
*
|
|
26
|
-
* - Initializes with an empty array and a random hash.
|
|
27
|
-
* - Returns a function that compares the current array to the previous one.
|
|
28
|
-
* - Updates the hash if any difference is detected.
|
|
29
|
-
*/
|
|
30
|
-
get computedHash() {
|
|
31
|
-
let currentValues: any[] = []
|
|
32
|
-
let currentHash = randomHash()
|
|
33
|
-
return (e: any[]) => {
|
|
34
|
-
let isDiff = false
|
|
35
|
-
|
|
36
|
-
// Check for differences in array existence, length, or elements.
|
|
37
|
-
isDiff = isDiff || ((!e) != (!currentValues))
|
|
38
|
-
isDiff = isDiff || (e?.length != currentValues?.length);
|
|
39
|
-
isDiff = isDiff || (e.some((f, i) => f != currentValues[i]));
|
|
40
|
-
|
|
41
|
-
// Update the hash if differences are found.
|
|
42
|
-
currentValues = e;
|
|
43
|
-
if (isDiff) {
|
|
44
|
-
currentHash = randomHash()
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return currentHash
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
return computedHash(e)
|
|
53
|
-
}
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import { debounce } from "./utils";
|
|
3
|
-
import { useState, useMemo, useEffect } from "react";
|
|
4
|
-
import type { Context } from "./ctx";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* useQuickSubscribe is a custom React hook for efficiently subscribing to specific properties of a context's data object.
|
|
8
|
-
*
|
|
9
|
-
* @template D - The shape of the context data.
|
|
10
|
-
* @param {Context<D> | undefined} ctx - The context object containing data and a subscribe method.
|
|
11
|
-
* @returns {Partial<D>} A proxy object that mirrors the context data, automatically subscribing to properties as they are accessed.
|
|
12
|
-
*
|
|
13
|
-
* This hook tracks which properties of the context data are accessed by the component and subscribes to updates for only those properties.
|
|
14
|
-
* When any of the subscribed properties change, the hook triggers a re-render. Subscriptions are managed and cleaned up automatically
|
|
15
|
-
* when the component unmounts or the context changes. This approach minimizes unnecessary re-renders and resource usage by only
|
|
16
|
-
* subscribing to the data that the component actually uses.
|
|
17
|
-
*
|
|
18
|
-
* Example usage:
|
|
19
|
-
* const {name} = useQuickSubscribe(userContext);
|
|
20
|
-
* // Accessing name will subscribe to changes in 'name' only
|
|
21
|
-
* return <div>{name}</div>;
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
export const useQuickSubscribe = <D>(
|
|
25
|
-
ctx: Context<D> | undefined
|
|
26
|
-
): {
|
|
27
|
-
[P in keyof D]?: D[P] | undefined;
|
|
28
|
-
} => {
|
|
29
|
-
|
|
30
|
-
const [, setCounter] = useState(0);
|
|
31
|
-
|
|
32
|
-
const { proxy, finalGetter, openGetter, clean } = useMemo(
|
|
33
|
-
() => {
|
|
34
|
-
|
|
35
|
-
const allKeys = new Set<keyof D>()
|
|
36
|
-
const allCompareValue: { [P in keyof D]?: D[P] | undefined; } = {}
|
|
37
|
-
const allUnsub = new Map()
|
|
38
|
-
|
|
39
|
-
const proxy = new Proxy(
|
|
40
|
-
ctx?.data as any,
|
|
41
|
-
{
|
|
42
|
-
get(target, p) {
|
|
43
|
-
if (isOpenGetter) {
|
|
44
|
-
allKeys.add(p as keyof D)
|
|
45
|
-
return allCompareValue[p as keyof D] = target[p];
|
|
46
|
-
} else {
|
|
47
|
-
throw new Error("now allow here")
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
) as any
|
|
52
|
-
|
|
53
|
-
let isOpenGetter = true;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
let onChange = debounce(() => {
|
|
57
|
-
if ([...allKeys.values()]
|
|
58
|
-
.some(k => allCompareValue[k] != ctx?.data?.[k])) {
|
|
59
|
-
setCounter(c => c + 1)
|
|
60
|
-
}
|
|
61
|
-
}, 0)
|
|
62
|
-
|
|
63
|
-
let openGetter = () => {
|
|
64
|
-
isOpenGetter = true
|
|
65
|
-
allKeys.clear()
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
let finalGetter = () => {
|
|
69
|
-
isOpenGetter = false;
|
|
70
|
-
|
|
71
|
-
[...allKeys.values()]
|
|
72
|
-
.filter(k => !allUnsub.has(k))
|
|
73
|
-
.forEach(k => {
|
|
74
|
-
allUnsub.set(k, ctx?.subscribe(k, onChange))
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
[...allUnsub.keys()]
|
|
78
|
-
.filter(k => !allKeys.has(k))
|
|
79
|
-
.forEach(k => {
|
|
80
|
-
let unsub = allUnsub.get(k)
|
|
81
|
-
unsub?.();
|
|
82
|
-
allUnsub.delete(k);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
let clean = () => {
|
|
88
|
-
openGetter();
|
|
89
|
-
finalGetter();
|
|
90
|
-
setCounter(c => c + 1)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return { proxy, finalGetter, openGetter, clean }
|
|
94
|
-
},
|
|
95
|
-
[ctx]
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
openGetter();
|
|
99
|
-
|
|
100
|
-
setTimeout(finalGetter, 0)
|
|
101
|
-
|
|
102
|
-
useEffect(
|
|
103
|
-
() => () => clean(),
|
|
104
|
-
[clean]
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
return proxy;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
};
|
package/src/state-utils/utils.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
// Debounce function
|
|
2
|
-
export function debounce<T extends (...args: any[]) => any>(
|
|
3
|
-
func: T,
|
|
4
|
-
wait: number
|
|
5
|
-
): ((...args: Parameters<T>) => void) & { cancel: any } {
|
|
6
|
-
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
7
|
-
|
|
8
|
-
let fn: Function & { cancel: any } = function (...args: Parameters<T>): void {
|
|
9
|
-
if (timeout) {
|
|
10
|
-
clearTimeout(timeout);
|
|
11
|
-
}
|
|
12
|
-
timeout = setTimeout(() => {
|
|
13
|
-
func(...args);
|
|
14
|
-
}, wait);
|
|
15
|
-
} as any;
|
|
16
|
-
|
|
17
|
-
fn.cancel = () => clearTimeout(timeout!);
|
|
18
|
-
|
|
19
|
-
return fn as any;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// Memoize function
|
|
23
|
-
export function memoize<T extends (...args: any[]) => any>(
|
|
24
|
-
func: T
|
|
25
|
-
): ((...args: Parameters<T>) => ReturnType<T>) & { cache: Map<string, ReturnType<T>> } {
|
|
26
|
-
|
|
27
|
-
const cache = new Map<string, ReturnType<T>>();
|
|
28
|
-
|
|
29
|
-
const cachedFunc: any = function (...args: Parameters<T>): ReturnType<T> {
|
|
30
|
-
const key = JSON.stringify(args);
|
|
31
|
-
if (cache.has(key)) {
|
|
32
|
-
return cache.get(key) as ReturnType<T>;
|
|
33
|
-
}
|
|
34
|
-
const result = func(...args);
|
|
35
|
-
cache.set(key, result);
|
|
36
|
-
return result;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
cachedFunc.cache = cache;
|
|
40
|
-
|
|
41
|
-
return cachedFunc
|
|
42
|
-
}
|
|
43
|
-
|
package/src/vite-env.d.ts
DELETED
package/tsconfig.json
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "esnext",
|
|
4
|
-
"module": "esnext",
|
|
5
|
-
"lib": [
|
|
6
|
-
"dom",
|
|
7
|
-
"esnext"
|
|
8
|
-
],
|
|
9
|
-
"moduleResolution": "node",
|
|
10
|
-
"jsx": "react-jsx",
|
|
11
|
-
"strict": true,
|
|
12
|
-
"esModuleInterop": true,
|
|
13
|
-
"skipLibCheck": true,
|
|
14
|
-
"forceConsistentCasingInFileNames": true,
|
|
15
|
-
"outDir": "./dist",
|
|
16
|
-
"declaration": true,
|
|
17
|
-
"sourceMap": true
|
|
18
|
-
},
|
|
19
|
-
"include": [
|
|
20
|
-
"vite/client",
|
|
21
|
-
"src"
|
|
22
|
-
],
|
|
23
|
-
"exclude": [
|
|
24
|
-
"node_modules",
|
|
25
|
-
"dist"
|
|
26
|
-
]
|
|
27
|
-
}
|
package/vite.config.dev.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from 'vite';
|
|
2
|
-
import react from '@vitejs/plugin-react';
|
|
3
|
-
|
|
4
|
-
export default defineConfig({
|
|
5
|
-
base: '/react-state-custom/',
|
|
6
|
-
plugins: [
|
|
7
|
-
react(),
|
|
8
|
-
],
|
|
9
|
-
build: {
|
|
10
|
-
target: 'esnext',
|
|
11
|
-
outDir: 'demo-dist',
|
|
12
|
-
},
|
|
13
|
-
server: {
|
|
14
|
-
port: 3000,
|
|
15
|
-
},
|
|
16
|
-
});
|
package/vite.config.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from 'vite';
|
|
2
|
-
import react from '@vitejs/plugin-react';
|
|
3
|
-
import dts from 'vite-plugin-dts'
|
|
4
|
-
import { analyzer } from 'vite-bundle-analyzer'
|
|
5
|
-
|
|
6
|
-
export default defineConfig({
|
|
7
|
-
plugins: [
|
|
8
|
-
react(),
|
|
9
|
-
dts({
|
|
10
|
-
include: ['src'],
|
|
11
|
-
}),
|
|
12
|
-
analyzer({
|
|
13
|
-
analyzerMode: "server",
|
|
14
|
-
openAnalyzer: true
|
|
15
|
-
}),
|
|
16
|
-
],
|
|
17
|
-
build: {
|
|
18
|
-
lib: {
|
|
19
|
-
entry: 'src/index.ts',
|
|
20
|
-
name: 'RState',
|
|
21
|
-
fileName: (format) => `index.${format}.js`,
|
|
22
|
-
formats: ['es', 'umd'],
|
|
23
|
-
},
|
|
24
|
-
rollupOptions: {
|
|
25
|
-
// Ensure to externalize deps that shouldn't be bundled
|
|
26
|
-
external: ['react', 'react-dom', 'react/jsx-runtime', 'react/jsx-dev-runtime'],
|
|
27
|
-
output: {
|
|
28
|
-
globals: {
|
|
29
|
-
'react': 'React',
|
|
30
|
-
'react-dom': 'ReactDOM',
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
sourcemap: true
|
|
35
|
-
},
|
|
36
|
-
server: {
|
|
37
|
-
port: 3000,
|
|
38
|
-
},
|
|
39
|
-
});
|