@dr.pogodin/react-global-state 0.10.0-alpha.1 → 0.10.0-alpha.3

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.
Files changed (58) hide show
  1. package/README.md +3 -327
  2. package/build/common/GlobalState.js +219 -0
  3. package/build/common/GlobalState.js.map +1 -0
  4. package/build/common/GlobalStateProvider.js +101 -0
  5. package/build/common/GlobalStateProvider.js.map +1 -0
  6. package/build/common/SsrContext.js +15 -0
  7. package/build/common/SsrContext.js.map +1 -0
  8. package/build/common/index.js +76 -0
  9. package/build/common/index.js.map +1 -0
  10. package/build/common/useAsyncCollection.js +61 -0
  11. package/build/common/useAsyncCollection.js.map +1 -0
  12. package/build/common/useAsyncData.js +204 -0
  13. package/build/common/useAsyncData.js.map +1 -0
  14. package/build/common/useGlobalState.js +113 -0
  15. package/build/common/useGlobalState.js.map +1 -0
  16. package/build/common/utils.js +44 -0
  17. package/build/common/utils.js.map +1 -0
  18. package/build/common/withGlobalStateType.js +42 -0
  19. package/build/common/withGlobalStateType.js.map +1 -0
  20. package/build/module/GlobalState.js +230 -0
  21. package/build/module/GlobalState.js.map +1 -0
  22. package/build/module/GlobalStateProvider.js +94 -0
  23. package/build/module/GlobalStateProvider.js.map +1 -0
  24. package/build/module/SsrContext.js +9 -0
  25. package/build/module/SsrContext.js.map +1 -0
  26. package/build/module/index.js +8 -0
  27. package/build/module/index.js.map +1 -0
  28. package/build/module/useAsyncCollection.js +56 -0
  29. package/build/module/useAsyncCollection.js.map +1 -0
  30. package/build/module/useAsyncData.js +199 -0
  31. package/build/module/useAsyncData.js.map +1 -0
  32. package/build/module/useGlobalState.js +108 -0
  33. package/build/module/useGlobalState.js.map +1 -0
  34. package/build/module/utils.js +35 -0
  35. package/build/module/utils.js.map +1 -0
  36. package/build/module/withGlobalStateType.js +35 -0
  37. package/build/module/withGlobalStateType.js.map +1 -0
  38. package/build/types/GlobalState.d.ts +75 -0
  39. package/build/types/GlobalStateProvider.d.ts +55 -0
  40. package/build/types/SsrContext.d.ts +6 -0
  41. package/build/types/index.d.ts +8 -0
  42. package/build/types/useAsyncCollection.d.ts +51 -0
  43. package/build/types/useAsyncData.d.ts +75 -0
  44. package/build/types/useGlobalState.d.ts +58 -0
  45. package/build/types/utils.d.ts +34 -0
  46. package/build/types/withGlobalStateType.d.ts +29 -0
  47. package/package.json +42 -41
  48. package/src/GlobalState.ts +283 -0
  49. package/src/GlobalStateProvider.tsx +127 -0
  50. package/src/SsrContext.ts +11 -0
  51. package/src/index.ts +33 -0
  52. package/src/useAsyncCollection.ts +96 -0
  53. package/src/useAsyncData.ts +295 -0
  54. package/src/useGlobalState.ts +156 -0
  55. package/src/utils.ts +64 -0
  56. package/src/withGlobalStateType.ts +170 -0
  57. package/tsconfig.json +9 -3
  58. package/tsconfig.types.json +14 -0
@@ -0,0 +1,156 @@
1
+ // Hook for updates of global state.
2
+
3
+ import { cloneDeep, isFunction } from 'lodash';
4
+ import { useEffect, useRef, useSyncExternalStore } from 'react';
5
+
6
+ import { Emitter } from '@dr.pogodin/js-utils';
7
+
8
+ import GlobalState from './GlobalState';
9
+ import { getGlobalState } from './GlobalStateProvider';
10
+
11
+ import {
12
+ type CallbackT,
13
+ type ForceT,
14
+ type TypeLock,
15
+ type ValueAtPathT,
16
+ type ValueOrInitializerT,
17
+ isDebugMode,
18
+ } from './utils';
19
+
20
+ export type SetterT<T> = React.Dispatch<React.SetStateAction<T>>;
21
+
22
+ type GlobalStateRef = {
23
+ emitter: Emitter<[]>;
24
+ globalState: GlobalState<unknown>;
25
+ path: null | string | undefined;
26
+ setter: SetterT<unknown>;
27
+ state: unknown;
28
+ watcher: CallbackT;
29
+ };
30
+
31
+ export type UseGlobalStateResT<T> = [T, SetterT<T>];
32
+
33
+ /**
34
+ * The primary hook for interacting with the global state, modeled after
35
+ * the standard React's
36
+ * [useState](https://reactjs.org/docs/hooks-reference.html#usestate).
37
+ * It subscribes a component to a given `path` of global state, and provides
38
+ * a function to update it. Each time the value at `path` changes, the hook
39
+ * triggers re-render of its host component.
40
+ *
41
+ * **Note:**
42
+ * - For performance, the library does not copy objects written to / read from
43
+ * global state paths. You MUST NOT manually mutate returned state values,
44
+ * or change objects already written into the global state, without explicitly
45
+ * clonning them first yourself.
46
+ * - State update notifications are asynchronous. When your code does multiple
47
+ * global state updates in the same React rendering cycle, all state update
48
+ * notifications are queued and dispatched together, after the current
49
+ * rendering cycle. In other words, in any given rendering cycle the global
50
+ * state values are "fixed", and all changes becomes visible at once in the
51
+ * next triggered rendering pass.
52
+ *
53
+ * @param path Dot-delimitered state path. It can be undefined to
54
+ * subscribe for entire state.
55
+ *
56
+ * Under-the-hood state values are read and written using `lodash`
57
+ * [_.get()](https://lodash.com/docs/4.17.15#get) and
58
+ * [_.set()](https://lodash.com/docs/4.17.15#set) methods, thus it is safe
59
+ * to access state paths which have not been created before.
60
+ * @param initialValue Initial value to set at the `path`, or its
61
+ * factory:
62
+ * - If a function is given, it will act similar to
63
+ * [the lazy initial state of the standard React's useState()](https://reactjs.org/docs/hooks-reference.html#lazy-initial-state):
64
+ * only if the value at `path` is `undefined`, the function will be executed,
65
+ * and the value it returns will be written to the `path`.
66
+ * - Otherwise, the given value itself will be written to the `path`,
67
+ * if the current value at `path` is `undefined`.
68
+ * @return It returs an array with two elements: `[value, setValue]`:
69
+ *
70
+ * - The `value` is the current value at given `path`.
71
+ *
72
+ * - The `setValue()` is setter function to write a new value to the `path`.
73
+ *
74
+ * Similar to the standard React's `useState()`, it supports
75
+ * [functional value updates](https://reactjs.org/docs/hooks-reference.html#functional-updates):
76
+ * if `setValue()` is called with a function as argument, that function will
77
+ * be called and its return value will be written to `path`. Otherwise,
78
+ * the argument of `setValue()` itself is written to `path`.
79
+ *
80
+ * Also, similar to the standard React's state setters, `setValue()` is
81
+ * stable function: it does not change between component re-renders.
82
+ */
83
+
84
+ function useGlobalState<StateT>(): UseGlobalStateResT<StateT>;
85
+
86
+ function useGlobalState<Forced extends ForceT | false = false, ValueT = unknown>(
87
+ path: null | string | undefined,
88
+ initialValue?: ValueOrInitializerT<TypeLock<Forced, never, ValueT>>,
89
+ ): UseGlobalStateResT<TypeLock<Forced, void, ValueT>>;
90
+
91
+ function useGlobalState<
92
+ StateT,
93
+ PathT extends null | string | undefined,
94
+ >(
95
+ path: PathT,
96
+ initialValue?: ValueOrInitializerT<ValueAtPathT<StateT, PathT, never>>
97
+ ): UseGlobalStateResT<ValueAtPathT<StateT, PathT, void>>;
98
+
99
+ function useGlobalState(
100
+ path?: null | string,
101
+ initialValue?: ValueOrInitializerT<unknown>,
102
+ ): UseGlobalStateResT<any> {
103
+ const globalState = getGlobalState();
104
+
105
+ const ref = useRef<GlobalStateRef>();
106
+ const rc: GlobalStateRef = ref.current || {
107
+ emitter: new Emitter(),
108
+ globalState,
109
+ path,
110
+ setter: (value) => {
111
+ const newState = isFunction(value) ? value(rc.state) : value;
112
+ if (process.env.NODE_ENV !== 'production' && isDebugMode()) {
113
+ /* eslint-disable no-console */
114
+ console.groupCollapsed(
115
+ `ReactGlobalState - useGlobalState setter triggered for path ${
116
+ rc.path || ''
117
+ }`,
118
+ );
119
+ console.log('New value:', cloneDeep(newState));
120
+ console.groupEnd();
121
+ /* eslint-enable no-console */
122
+ }
123
+ rc.globalState.set<ForceT, unknown>(rc.path, newState);
124
+ },
125
+ state: isFunction(initialValue) ? initialValue() : initialValue,
126
+ watcher: () => {
127
+ const state = rc.globalState.get(rc.path);
128
+ if (state !== rc.state) rc.emitter.emit();
129
+ },
130
+ };
131
+ ref.current = rc;
132
+
133
+ rc.globalState = globalState;
134
+ rc.path = path;
135
+
136
+ rc.state = useSyncExternalStore(
137
+ (cb) => rc.emitter.addListener(cb),
138
+ () => rc.globalState.get<ForceT, unknown>(rc.path, { initialValue }),
139
+ () => rc.globalState.get<ForceT, unknown>(rc.path, { initialValue, initialState: true }),
140
+ );
141
+
142
+ useEffect(() => {
143
+ const { watcher } = ref.current!;
144
+ globalState.watch(watcher);
145
+ watcher();
146
+ return () => globalState.unWatch(watcher);
147
+ }, [globalState]);
148
+
149
+ useEffect(() => {
150
+ ref.current!.watcher();
151
+ }, [path]);
152
+
153
+ return [rc.state, rc.setter];
154
+ }
155
+
156
+ export default useGlobalState;
package/src/utils.ts ADDED
@@ -0,0 +1,64 @@
1
+ import { type GetFieldType } from 'lodash';
2
+
3
+ export type CallbackT = () => void;
4
+
5
+ // TODO: This (ForceT & TypeLock) probably should be moved to JS Utils lib.
6
+
7
+ // This type is used to "unlocked" special generic overrides around the library.
8
+ declare const force: unique symbol;
9
+ export type ForceT = typeof force;
10
+
11
+ export type TypeLock<
12
+ Unlocked extends ForceT | false,
13
+ LockedT extends never | void,
14
+ UnlockedT,
15
+ > = Unlocked extends ForceT ? UnlockedT : LockedT;
16
+
17
+ /**
18
+ * Given the type of state, `StateT`, and the type of state path, `PathT`,
19
+ * it evaluates the type of value at that path of the state, if it can be
20
+ * evaluated from the path type (it is possible when `PathT` is a string
21
+ * literal type, and `StateT` elements along this path have appropriate
22
+ * types); otherwise it falls back to the specified `UnknownT` type,
23
+ * which should be set either `never` (for input arguments), or `void`
24
+ * (for return types) - `never` and `void` in those places forbid assignments,
25
+ * and are not auto-inferred to more permissible types.
26
+ *
27
+ * BEWARE: When StateT is any the construct resolves to any for any string
28
+ * paths.
29
+ */
30
+ export type ValueAtPathT<
31
+ StateT,
32
+ PathT extends null | string | undefined,
33
+ UnknownT extends never | undefined | void,
34
+ > = unknown extends StateT
35
+ ? UnknownT
36
+ : string extends PathT
37
+ ? UnknownT
38
+ : PathT extends null | undefined
39
+ ? StateT
40
+ : GetFieldType<StateT, PathT> extends undefined
41
+ ? UnknownT : GetFieldType<StateT, PathT>;
42
+
43
+ export type ValueOrInitializerT<ValueT> = ValueT | (() => ValueT);
44
+
45
+ /**
46
+ * Returns 'true' if debug logging should be performed; 'false' otherwise.
47
+ *
48
+ * BEWARE: The actual safeguards for the debug logging still should read
49
+ * if (process.env.NODE_ENV !== 'production' && isDebugMode()) {
50
+ * // Some debug logging
51
+ * }
52
+ * to ensure that debug code is stripped out by Webpack in production mode.
53
+ *
54
+ * @returns
55
+ * @ignore
56
+ */
57
+ export function isDebugMode(): boolean {
58
+ try {
59
+ return process.env.NODE_ENV !== 'production'
60
+ && !!process.env.REACT_GLOBAL_STATE_DEBUG;
61
+ } catch (error) {
62
+ return false;
63
+ }
64
+ }
@@ -0,0 +1,170 @@
1
+ import {
2
+ type ForceT,
3
+ type TypeLock,
4
+ type ValueAtPathT,
5
+ type ValueOrInitializerT,
6
+ } from './utils';
7
+
8
+ import GlobalStateProvider, {
9
+ getGlobalState,
10
+ getSsrContext,
11
+ } from './GlobalStateProvider';
12
+
13
+ import SsrContext from './SsrContext';
14
+
15
+ import useGlobalState, {
16
+ type UseGlobalStateResT,
17
+ } from './useGlobalState';
18
+
19
+ import useAsyncCollection, {
20
+ type AsyncCollectionLoaderT,
21
+ } from './useAsyncCollection';
22
+
23
+ import useAsyncData, {
24
+ type AsyncDataLoaderT,
25
+ type DataInEnvelopeAtPathT,
26
+ type UseAsyncDataOptionsT,
27
+ type UseAsyncDataResT,
28
+ } from './useAsyncData';
29
+
30
+ interface NarrowedUseGlobalStateI<StateT> {
31
+ (): UseGlobalStateResT<StateT>;
32
+
33
+ <PathT extends null | string | undefined>(
34
+ path: PathT,
35
+ initialValue?: ValueOrInitializerT<ValueAtPathT<StateT, PathT, never>>,
36
+ ): UseGlobalStateResT<ValueAtPathT<StateT, PathT, void>>;
37
+
38
+ <Forced extends ForceT | false = false, ValueT = unknown>(
39
+ path: null | string | undefined,
40
+ initialValue?: ValueOrInitializerT<TypeLock<Forced, never, ValueT>>,
41
+ ): UseGlobalStateResT<TypeLock<Forced, void, ValueT>>;
42
+ }
43
+
44
+ interface NarrowedUseAsyncDataI<StateT> {
45
+ <PathT extends null | string | undefined>(
46
+ path: PathT,
47
+ loader: AsyncDataLoaderT<DataInEnvelopeAtPathT<StateT, PathT>>,
48
+ options?: UseAsyncDataOptionsT,
49
+ ): UseAsyncDataResT<DataInEnvelopeAtPathT<StateT, PathT>>;
50
+
51
+ <Forced extends ForceT | false = false, DataT = unknown>(
52
+ path: null | string | undefined,
53
+ loader: AsyncDataLoaderT<TypeLock<Forced, void, DataT>>,
54
+ options?: UseAsyncDataOptionsT,
55
+ ): UseAsyncDataResT<TypeLock<Forced, never, DataT>>;
56
+ }
57
+
58
+ interface NarrowedUseAsyncCollectionI<StateT> {
59
+ <PathT extends null | string | undefined>(
60
+ id: string,
61
+ path: PathT,
62
+ loader: AsyncCollectionLoaderT<DataInEnvelopeAtPathT<StateT, PathT>>,
63
+ options?: UseAsyncDataOptionsT,
64
+ ): UseAsyncDataResT<DataInEnvelopeAtPathT<StateT, PathT>>;
65
+
66
+ <Forced extends ForceT | false = false, DataT = unknown>(
67
+ id: string,
68
+ path: null | string | undefined,
69
+ loader: AsyncCollectionLoaderT<TypeLock<Forced, void, DataT>>,
70
+ options?: UseAsyncDataOptionsT,
71
+ ): UseAsyncDataResT<DataT>;
72
+ }
73
+
74
+ type WithGlobalStateTypeResT<
75
+ StateT,
76
+ SsrContextT extends SsrContext<StateT> = SsrContext<StateT>,
77
+ > = {
78
+ getGlobalState: typeof getGlobalState<StateT, SsrContextT>,
79
+ getSsrContext: typeof getSsrContext<SsrContextT>,
80
+ GlobalStateProvider: typeof GlobalStateProvider<StateT, SsrContextT>,
81
+ // SsrContext: SsrContext<StateT>,
82
+ useAsyncCollection: NarrowedUseAsyncCollectionI<StateT>,
83
+ useAsyncData: NarrowedUseAsyncDataI<StateT>,
84
+ useGlobalState: NarrowedUseGlobalStateI<StateT>,
85
+ };
86
+
87
+ export default function withGlobalStateType<
88
+ StateT,
89
+ SsrContextT extends SsrContext<StateT> = SsrContext<StateT>,
90
+ >(): WithGlobalStateTypeResT<StateT, SsrContextT> {
91
+ // These wrap useGlobalState() with locked-in StateT type.
92
+
93
+ function useGlobalStateWrap(): UseGlobalStateResT<StateT>;
94
+
95
+ function useGlobalStateWrap<PathT extends null | string | undefined>(
96
+ path: PathT,
97
+ initialValue?: ValueOrInitializerT<ValueAtPathT<StateT, PathT, never>>,
98
+ ): UseGlobalStateResT<ValueAtPathT<StateT, PathT, void>>;
99
+
100
+ function useGlobalStateWrap<Forced extends ForceT | false = false, ValueT = unknown>(
101
+ path: null | string | undefined,
102
+ initialValue?: ValueOrInitializerT<TypeLock<Forced, never, ValueT>>,
103
+ ): UseGlobalStateResT<TypeLock<Forced, void, ValueT>>;
104
+
105
+ function useGlobalStateWrap(
106
+ path?: null | string,
107
+ initialValue?: ValueOrInitializerT<unknown>,
108
+ ): UseGlobalStateResT<any> {
109
+ return useGlobalState<ForceT, unknown>(path, initialValue);
110
+ }
111
+
112
+ // These overloads & implementation wrap useAsyncData() hook to lock-in its
113
+ // underlying StateT.
114
+
115
+ function useAsyncDataWrap<PathT extends null | string | undefined>(
116
+ path: PathT,
117
+ loader: AsyncDataLoaderT<DataInEnvelopeAtPathT<StateT, PathT>>,
118
+ options?: UseAsyncDataOptionsT,
119
+ ): UseAsyncDataResT<DataInEnvelopeAtPathT<StateT, PathT>>;
120
+
121
+ function useAsyncDataWrap<Forced extends ForceT | false = false, DataT = unknown>(
122
+ path: null | string | undefined,
123
+ loader: AsyncDataLoaderT<TypeLock<Forced, void, DataT>>,
124
+ options?: UseAsyncDataOptionsT,
125
+ ): UseAsyncDataResT<TypeLock<Forced, never, DataT>>;
126
+
127
+ function useAsyncDataWrap<DataT>(
128
+ path: null | string | undefined,
129
+ loader: AsyncDataLoaderT<DataT>,
130
+ options: UseAsyncDataOptionsT = {},
131
+ ): UseAsyncDataResT<DataT> {
132
+ return useAsyncData<ForceT, DataT>(path, loader, options);
133
+ }
134
+
135
+ // These overloads & implementation wrap useAsyncCollection() hook to lock-in
136
+ // its underlying StateT.
137
+
138
+ function useAsyncCollectionWrap<PathT extends null | string | undefined>(
139
+ id: string,
140
+ path: PathT,
141
+ loader: AsyncCollectionLoaderT<DataInEnvelopeAtPathT<StateT, PathT>>,
142
+ options?: UseAsyncDataOptionsT,
143
+ ): UseAsyncDataResT<DataInEnvelopeAtPathT<StateT, PathT>>;
144
+
145
+ function useAsyncCollectionWrap<Forced extends ForceT | false = false, DataT = unknown>(
146
+ id: string,
147
+ path: null | string | undefined,
148
+ loader: AsyncCollectionLoaderT<TypeLock<Forced, void, DataT>>,
149
+ options?: UseAsyncDataOptionsT,
150
+ ): UseAsyncDataResT<DataT>;
151
+
152
+ function useAsyncCollectionWrap<DataT>(
153
+ id: string,
154
+ path: null | string | undefined,
155
+ loader: AsyncCollectionLoaderT<DataT>,
156
+ options: UseAsyncDataOptionsT = {},
157
+ ): UseAsyncDataResT<DataT> {
158
+ return useAsyncCollection<ForceT, DataT>(id, path, loader, options);
159
+ }
160
+
161
+ return {
162
+ getGlobalState: getGlobalState<StateT, SsrContextT>,
163
+ getSsrContext: getSsrContext<SsrContextT>,
164
+ GlobalStateProvider: GlobalStateProvider<StateT, SsrContextT>,
165
+ // SsrContext, /* CustomSsrContext || SsrContext, */
166
+ useAsyncCollection: useAsyncCollectionWrap,
167
+ useAsyncData: useAsyncDataWrap,
168
+ useGlobalState: useGlobalStateWrap,
169
+ };
170
+ }
package/tsconfig.json CHANGED
@@ -1,9 +1,15 @@
1
1
  {
2
2
  "extends": "@tsconfig/recommended",
3
- "include": ["src/**/*"],
4
3
  "compilerOptions": {
5
4
  "declaration": true,
6
5
  "jsx": "react-jsx",
7
- "outDir": "build"
8
- }
6
+ "module": "ESNext",
7
+ "moduleResolution": "Node",
8
+ "outDir": "build",
9
+ "paths": {
10
+ "*": ["./*"],
11
+ },
12
+ "strict": true,
13
+ "verbatimModuleSyntax": true,
14
+ },
9
15
  }
@@ -0,0 +1,14 @@
1
+ {
2
+ "extends": "@tsconfig/recommended",
3
+ "include": ["src"],
4
+ "compilerOptions": {
5
+ "declaration": true,
6
+ "emitDeclarationOnly": true,
7
+ "jsx": "react-jsx",
8
+ "module": "ESNext",
9
+ "moduleResolution": "Node",
10
+ "outDir": "build/types",
11
+ "strict": true,
12
+ "verbatimModuleSyntax": true,
13
+ }
14
+ }