react-state-custom 1.0.9 → 1.0.11

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.
@@ -3,6 +3,35 @@ import { useDataContext, useDataSourceMultiple, type Context } from "./ctx"
3
3
 
4
4
 
5
5
 
6
+ /**
7
+ * createRootCtx
8
+ *
9
+ * Factory that creates a headless "Root" component and companion hooks for a context namespace.
10
+ * It derives a unique context name from a base `name` and a props object `U`, then publishes
11
+ * a computed state `V` (from `useFn`) to that context.
12
+ *
13
+ * Usage (manual mounting):
14
+ * ```
15
+ * const { Root, useCtxState } = createRootCtx('user-state', useUserState)
16
+ * ...
17
+ * // Mount exactly one Root per unique props combination
18
+ * <Root userId={id} />
19
+ * ...
20
+ * // Read anywhere ,using the same props shape
21
+ * const user = useCtxState({ userId: id })
22
+ *```
23
+ * Strict vs lenient consumers:
24
+ * - useCtxStateStrict(props) throws if a matching Root is not mounted.
25
+ * - useCtxState(props) logs an error (after 1s) instead of throwing.
26
+ *
27
+ * Multiple instances safety:
28
+ * - Mounting more than one Root with the same resolved context name throws (guards accidental duplicates).
29
+ *
30
+ * Name resolution notes:
31
+ * - The context name is built from `name` + sorted key/value pairs of `props` (U), joined by "-".
32
+ * - Prefer stable, primitive props to avoid collisions; if you need automation, pair with `createAutoCtx` and
33
+ * mount a single <AutoRootCtx Wrapper={ErrorBoundary} /> at the app root so you don't manually mount `Root`.
34
+ */
6
35
  export const createRootCtx = <U extends object, V extends object>(name: string, useFn: (e: U) => V) => {
7
36
 
8
37
  const resolveCtxName = (e: U) => [
@@ -17,9 +46,9 @@ export const createRootCtx = <U extends object, V extends object>(name: string,
17
46
 
18
47
 
19
48
  const RootState: React.FC<U> = (e: U) => {
49
+ const state = useFn(e)
20
50
  const ctxName = resolveCtxName(e)
21
51
  const ctx = useDataContext<V>(ctxName)
22
- const state = useFn(e)
23
52
  const stack = useMemo(() => new Error().stack, [])
24
53
 
25
54
  useDataSourceMultiple(
@@ -45,6 +74,10 @@ export const createRootCtx = <U extends object, V extends object>(name: string,
45
74
  return {
46
75
  resolveCtxName,
47
76
  Root: RootState,
77
+ /**
78
+ * Strict consumer: throws if the corresponding Root for these props isn't mounted.
79
+ * Use in development/tests to fail fast when wiring is incorrect.
80
+ */
48
81
  useCtxStateStrict: (e: U): Context<V> => {
49
82
  const ctxName = resolveCtxName(e)
50
83
 
@@ -60,6 +93,10 @@ export const createRootCtx = <U extends object, V extends object>(name: string,
60
93
 
61
94
  return useDataContext<V>(ctxName)
62
95
  },
96
+ /**
97
+ * Lenient consumer: schedules a console.error if the Root isn't mounted instead of throwing.
98
+ * Useful in production to avoid hard crashes while still surfacing misconfiguration.
99
+ */
63
100
  useCtxState: (e: U): Context<V> => {
64
101
  const ctxName = resolveCtxName(e)
65
102
 
@@ -1,7 +1,7 @@
1
- import event from "events"
2
- import { debounce, memoize, throttle } from "lodash-es"
1
+ import { EventEmitter } from "events"
2
+ import { debounce, memoize } from "lodash-es"
3
3
  import { useEffect, useMemo, useState } from "react"
4
- import { useObjHash } from "./useObjectHash"
4
+ import { useArrayHash } from "./useArrayHash"
5
5
 
6
6
 
7
7
 
@@ -19,7 +19,7 @@ export class Context<D> {
19
19
  this.event.setMaxListeners(100)
20
20
  }
21
21
 
22
- private event = new event.EventEmitter()
22
+ private event = new EventEmitter()
23
23
 
24
24
  /**
25
25
  * The current data held by the context.
@@ -150,7 +150,9 @@ export const useDataSubscribe = <D, K extends keyof D>(ctx: Context<D> | undefin
150
150
 
151
151
  useEffect(() => {
152
152
  if (ctx) {
153
- let callback = debounceTime == 0 ? (value: any) => setState({ value } as any) : debounce((value: any) => setState({ value } as any), debounceTime)
153
+ let callback = debounceTime == 0
154
+ ? (value: any) => setState({ value } as any)
155
+ : debounce((value: any) => setState({ value } as any), debounceTime)
154
156
  let unsub = ctx.subscribe(key, callback)
155
157
  value != ctx.data[key] && setState({ value: ctx.data[key] })
156
158
  return () => {
@@ -211,7 +213,7 @@ export const useDataSourceMultiple = <D, T extends readonly (keyof D)[]>(
211
213
  ctx.data[key] != value && ctx.publish(key, value)
212
214
  }
213
215
  }
214
- }, [ctx, useObjHash(entries.flat())])
216
+ }, [ctx, useArrayHash(entries.flat())])
215
217
 
216
218
  useRegistryChecker(ctx, ...entries.map(e => e[0]) as any)
217
219
 
@@ -264,13 +266,13 @@ export const useDataSubscribeMultiple = <D, K extends keyof D>(
264
266
  /**
265
267
  * React hook to subscribe to multiple context values with throttling.
266
268
  * @param ctx - The context instance.
267
- * @param debounceTime - Throttle time in ms (default 100).
269
+ * @param debounceTime - Debounce time in ms (default 50).
268
270
  * @param keys - Keys to subscribe to.
269
271
  * @returns Array of current values for the keys.
270
272
  */
271
273
  export const useDataSubscribeMultipleWithDebounce = <D, K extends (keyof D)[]>(
272
274
  ctx: Context<D> | undefined,
273
- debounceTime = 100,
275
+ debounceTime = 50,
274
276
  ...keys: K
275
277
  ): { [i in keyof K]: D[K[i]] | undefined } => {
276
278
  //@ts-check
@@ -281,7 +283,7 @@ export const useDataSubscribeMultipleWithDebounce = <D, K extends (keyof D)[]>(
281
283
  useEffect(() => {
282
284
  if (ctx) {
283
285
  let prevValues = returnValues
284
- const callback = throttle(() => {
286
+ const callback = debounce(() => {
285
287
  let currentValues = keys.map(key => ctx?.data?.[key])
286
288
  if (keys.some((key, i) => prevValues[i] != currentValues[i])) {
287
289
  prevValues = currentValues
@@ -0,0 +1,53 @@
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
+ }
@@ -3,19 +3,42 @@ import { useRef } from "react"
3
3
 
4
4
  const randomHash = () => Math.random().toString().slice(2)
5
5
 
6
- export const useObjHash = (e: any[]): string => {
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 => {
7
21
 
8
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
+ */
9
30
  get computedHash() {
10
31
  let currentValues: any[] = []
11
32
  let currentHash = randomHash()
12
33
  return (e: any[]) => {
13
34
  let isDiff = false
14
35
 
36
+ // Check for differences in array existence, length, or elements.
15
37
  isDiff = isDiff || ((!e) != (!currentValues))
16
38
  isDiff = isDiff || (e?.length != currentValues?.length);
17
39
  isDiff = isDiff || (e.some((f, i) => f != currentValues[i]));
18
40
 
41
+ // Update the hash if differences are found.
19
42
  currentValues = e;
20
43
  if (isDiff) {
21
44
  currentHash = randomHash()
@@ -1,3 +1,8 @@
1
+
2
+ import { debounce } from "lodash-es";
3
+ import { useState, useMemo, useEffect } from "react";
4
+ import type { Context } from "./ctx";
5
+
1
6
  /**
2
7
  * useQuickSubscribe is a custom React hook for efficiently subscribing to specific properties of a context's data object.
3
8
  *
@@ -16,11 +21,6 @@
16
21
  * return <div>{name}</div>;
17
22
  */
18
23
 
19
- import { debounce } from "lodash-es";
20
- import { useState, useMemo, useEffect } from "react";
21
- import type { Context } from "./ctx";
22
-
23
-
24
24
  export const useQuickSubscribe = <D>(
25
25
  ctx: Context<D> | undefined
26
26
  ): {
@@ -29,85 +29,80 @@ export const useQuickSubscribe = <D>(
29
29
 
30
30
  const [, setCounter] = useState(0);
31
31
 
32
- const { proxy, clean, handleOnChange } = useMemo(
32
+ const { proxy, finalGetter, openGetter, clean } = useMemo(
33
33
  () => {
34
34
 
35
35
  const allKeys = new Set<keyof D>()
36
- const allUnsubInstance = new Map<keyof D, Function>()
37
- const allCompareValue = ({} as Partial<D>)
38
-
39
- const handleOnChange = debounce(() => {
40
- console.log("handleOnChange",allCompareValue)
41
- if (ctx && Object
42
- .keys(allCompareValue)
43
- .some((i: any) => allCompareValue[i as keyof D] != ctx?.data[i as keyof D])) {
44
- setCounter(c => c + 1);
45
- }
46
- }, 1)
47
-
48
- const handleChangeKey = debounce(() => {
49
- if (ctx) {
50
- console.log("handleChangeKey")
51
- let shouldUpdate = false;
52
- let keyToDelete: (keyof D)[] = []
53
-
54
- for (let [k, unsub] of allUnsubInstance) {
55
- if (!allKeys.has(k)) {
56
- console.log("Remove", k)
57
- unsub?.();
58
- keyToDelete.push(k)
59
- }
60
- }
61
- keyToDelete.forEach(k => {
62
- allUnsubInstance.delete(k);
63
- delete allCompareValue[k];
64
- })
65
- for (let k of allKeys) {
66
- if (!allUnsubInstance.has(k)) {
67
- console.log("Add ", k)
68
- const sub = ctx.subscribe(k, handleOnChange);
69
- allUnsubInstance.set(k, sub);
70
- shouldUpdate = true;
71
- }
72
- }
73
- allKeys.clear()
74
- if (shouldUpdate) handleOnChange?.();
75
- }
76
- }, 0)
77
-
78
- const handleAddKey = (p: keyof D) => {
79
- allKeys.add(p);
80
- handleChangeKey();
81
- }
36
+ const allCompareValue: { [P in keyof D]?: D[P] | undefined; } = {}
37
+ const allUnsub = new Map()
82
38
 
83
39
  const proxy = new Proxy(
84
40
  ctx?.data as any,
85
41
  {
86
42
  get(target, p) {
87
- handleAddKey(p as keyof D);
88
- console.log({ [p]: target[p] });
89
- return allCompareValue[p as keyof D] = 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
+ }
90
49
  }
91
50
  }
92
51
  ) as any
93
52
 
94
- const clean = () => {
95
- console.log("Clean", allKeys)
96
- handleChangeKey?.()
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()
97
66
  }
98
67
 
99
- console.log("NEW")
100
- return { proxy, clean, handleOnChange }
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 }
101
94
  },
102
95
  [ctx]
103
96
  )
104
97
 
105
- useEffect(() => () => clean?.(), [clean])
98
+ openGetter();
106
99
 
107
- // useEffect(() => {
108
- // let i = setInterval(handleOnChange, 5000);
109
- // return () => clearInterval(i);
110
- // },[handleOnChange])
100
+ setTimeout(finalGetter, 0)
101
+
102
+ useEffect(
103
+ () => () => clean(),
104
+ [clean]
105
+ )
111
106
 
112
107
  return proxy;
113
108
 
package/vite.config.ts CHANGED
@@ -1,13 +1,18 @@
1
1
  import { defineConfig } from 'vite';
2
2
  import react from '@vitejs/plugin-react';
3
3
  import dts from 'vite-plugin-dts'
4
+ import { analyzer } from 'vite-bundle-analyzer'
4
5
 
5
6
  export default defineConfig({
6
7
  plugins: [
7
8
  react(),
8
9
  dts({
9
10
  include: ['src'],
10
- })
11
+ }),
12
+ analyzer({
13
+ analyzerMode: "server",
14
+ openAnalyzer: true
15
+ }),
11
16
  ],
12
17
  build: {
13
18
  lib: {
@@ -21,11 +26,12 @@ export default defineConfig({
21
26
  external: ['react', 'react-dom'],
22
27
  output: {
23
28
  globals: {
24
- react: 'React',
29
+ 'react': 'React',
25
30
  'react-dom': 'ReactDOM',
26
31
  },
27
32
  },
28
33
  },
34
+ sourcemap:true
29
35
  },
30
36
  server: {
31
37
  port: 3000,
@@ -1,7 +0,0 @@
1
- import { default as React } from 'react';
2
- interface MyComponentProps {
3
- title: string;
4
- description?: string;
5
- }
6
- declare const MyComponent: React.FC<MyComponentProps>;
7
- export default MyComponent;
@@ -1,2 +0,0 @@
1
- import { Context } from './ctx';
2
- export declare const useQuickSubscribeV2: <D>(ctx: Context<D> | undefined) => { [P in keyof D]?: D[P] | undefined; };
@@ -1,17 +0,0 @@
1
- import React from 'react';
2
-
3
- interface MyComponentProps {
4
- title: string;
5
- description?: string;
6
- }
7
-
8
- const MyComponent: React.FC<MyComponentProps> = ({ title, description }) => {
9
- return (
10
- <div>
11
- <h1>{title}</h1>
12
- {description && <p>{description}</p>}
13
- </div>
14
- );
15
- };
16
-
17
- export default MyComponent;
@@ -1,93 +0,0 @@
1
-
2
- import { debounce } from "lodash-es";
3
- import { useState, useMemo, useEffect } from "react";
4
- import type { Context } from "./ctx";
5
-
6
-
7
- export const useQuickSubscribeV2 = <D>(
8
- ctx: Context<D> | undefined
9
- ): {
10
- [P in keyof D]?: D[P] | undefined;
11
- } => {
12
-
13
- const [, setCounter] = useState(0);
14
-
15
- const { proxy, finalGetter, openGetter, clean } = useMemo(
16
- () => {
17
-
18
- const allKeys = new Set<keyof D>()
19
- const allCompareValue: { [P in keyof D]?: D[P] | undefined; } = {}
20
- const allUnsub = new Map()
21
-
22
- const proxy = new Proxy(
23
- ctx?.data as any,
24
- {
25
- get(target, p) {
26
- if (isOpenGetter) {
27
- allKeys.add(p as keyof D)
28
- return allCompareValue[p as keyof D] = target[p];
29
- } else {
30
- throw new Error("now allow here")
31
- }
32
- }
33
- }
34
- ) as any
35
-
36
- let isOpenGetter = true;
37
-
38
-
39
- let onChange = debounce(() => {
40
- if ([...allKeys.values()]
41
- .some(k => allCompareValue[k] != ctx?.data?.[k])) {
42
- setCounter(c => c + 1)
43
- }
44
- }, 0)
45
-
46
- let openGetter = () => {
47
- isOpenGetter = true
48
- allKeys.clear()
49
- }
50
-
51
- let finalGetter = () => {
52
- isOpenGetter = false;
53
-
54
- [...allKeys.values()]
55
- .filter(k => !allUnsub.has(k))
56
- .forEach(k => {
57
- allUnsub.set(k, ctx?.subscribe(k, onChange))
58
- });
59
-
60
- [...allUnsub.keys()]
61
- .filter(k => !allKeys.has(k))
62
- .forEach(k => {
63
- let unsub = allUnsub.get(k)
64
- unsub?.();
65
- allUnsub.delete(k);
66
- });
67
-
68
- }
69
-
70
- let clean = () => {
71
- openGetter();
72
- finalGetter();
73
- setCounter(c => c + 1)
74
- }
75
-
76
- return { proxy, finalGetter, openGetter, clean }
77
- },
78
- [ctx]
79
- )
80
-
81
- openGetter();
82
-
83
- setTimeout(finalGetter, 0)
84
-
85
- useEffect(
86
- () => () => clean(),
87
- [clean]
88
- )
89
-
90
- return proxy;
91
-
92
-
93
- };