@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
package/README.md CHANGED
@@ -10,332 +10,8 @@ State of the art approach to the global state and asynchronous data management
10
10
  in React applications, powered by hooks and Context API. Simple, efficient, with
11
11
  full server-side rendering (SSR) support.
12
12
 
13
- [Library Reference](https://dr.pogodin.studio/docs/react-global-state/index.html) •
14
- [Blog Article](https://dr.pogodin.studio/dev-blog/the-global-state-in-react-designed-right)
13
+ TypeScript support since v0.10.0.
15
14
 
16
- [![Sponsor](https://raw.githubusercontent.com/birdofpreyru/react-global-state/master/.README/sponsor.svg)](https://github.com/sponsors/birdofpreyru)
17
-
18
- ### Motivation
19
-
20
- The motivation and vision is to bring to the table all useful features
21
- of Redux, without related development overheads, like the amount of required
22
- boilerplate code, and the efforts needed to design and maintain actions
23
- and reducers.
24
-
25
- With this library, the introduction of a datum (data piece), shared across
26
- different application components, is as easy as using the local React state:
27
-
28
- ```jsx
29
- function SampleReactComponent() {
30
- const [data, setData] = useGlobalState('data.storage.path', initialValue);
31
-
32
- /* `data` value can be updating by calling `setData(newData)` anywhere inside
33
- * the component code, including inside hooks like `useEffect(..)` or some
34
- * event handlers. */
35
-
36
- return /* Some JSX markup. */;
37
- }
38
- ```
39
-
40
- Relying on async data, e.g. loading into the state data from a 3-rd party API,
41
- is the same easy:
42
-
43
- ```jsx
44
- async function loader() {
45
-
46
- /* Some async operation to get data, like a call to a 3-rd party API. */
47
-
48
- return data;
49
- }
50
-
51
- function SampleReactComponent() {
52
- const { data, loading, timestamp } = useAsyncData('data.envelope.path', loader);
53
-
54
- /* `data` holds the data loaded into the global state, if they are fresh enough;
55
- * `loading` signals that data loading (or silent re-loading) is in-progress;
56
- * `timestamp` is the timestamp of currently loaded `data`. */
57
-
58
- return /* Some JSX markup. */
59
- }
60
- ```
61
- ⇑ Behind the scene, the library takes care about updating the component
62
- when the data loading starts and ends, also about the timestamps, automatic
63
- reloading, and garbage collection of aged data.
64
-
65
- Related closely to async data is the server-side rendering (SSR). This library
66
- takes it into account, and provides a flexible way to implement SSR with loading
67
- of some, or all async data at the server side.
68
-
69
- ### Setup
70
-
71
- 1. <a name="base-setup"></a> The base setup is simple: just wrap your app into
72
- the `<GlobalStateProvider>` component, provided by this library, and you'll
73
- be able to use any library hooks within its child hierarchy.
74
-
75
- ```jsx
76
- /* The minimal example of the library setup and usage. */
77
-
78
- import React from 'react';
79
- import {
80
- GlobalStateProvider,
81
- useAsyncData,
82
- useGlobalState,
83
- } from '@dr.pogodin/react-global-state';
84
-
85
- /* Example of component relying on the global state. */
86
-
87
- function SampleComponent() {
88
- const [value, setValue] = useGlobalState('sample.component', 0);
89
- return (
90
- <button onClick={() => setValue(1 + value)}>
91
- {value}
92
- </button>
93
- );
94
- }
95
-
96
- /* Example of component relying on async data in the global state. */
97
-
98
- async function sampleDataLoader() {
99
- return new Promise((resolve) => {
100
- setTimeout(() => resolve('Sample Data'), 500);
101
- });
102
- }
103
-
104
- function SampleAsyncComponent() {
105
- const { data, loading } = useAsyncData('sample.async-component', sampleDataLoader);
106
- return data;
107
- }
108
-
109
- /* Example of the root app component, providing the state. */
110
-
111
- export default function SampleApp() {
112
- return (
113
- <GlobalStateProvider>
114
- <SampleComponent />
115
- <SampleAsyncComponent />
116
- </GlobalStateProvider>
117
- );
118
- }
119
- ```
120
-
121
- Multiple, or nested `<GlobalStateProvider>` instances are allowed, and they
122
- will provide independent global states to its children (shadowing parent ones,
123
- in the case of nesting). However, the current SSR implementation assumes
124
- a single `<GlobalStateProvider>` at the app root. Multiple providers won't
125
- break it, but won't be a part of SSR data loading either.
126
-
127
- This setup is fine to run both at the client, and at the server-side, but
128
- in the case of server-side rendering, the library won't run any async data
129
- fetching, thus rendering pages with the initial global state; e.g. in
130
- the example above the `<SampleAsyncComponent>` will be rendered as an empty
131
- node, as `data` will be `undefined`, and `loading` will be `false`.
132
- To handle SSR better, and to have `<SampleAsyncComponent>` rendered as
133
- `Sample Data` even at the server-side, you need the following, a bit more
134
- complex, setup.
135
-
136
- 2. Advanced setup with the server-side rendering support is demonstrated below,
137
- assuming that `<SampleComponent>`, `sampleDataLoader(..)`,
138
- and `<SampleAsyncComponent>` are defined the same way as in the previous
139
- example, and `<SampleApp>` component itself does not include
140
- the `<GlobalStateProvider>`, i.e.
141
-
142
- ```jsx
143
- export default function SampleApp() {
144
- return (
145
- <React.Fragment>
146
- <SampleComponent />
147
- <SampleAsyncComponent />
148
- </React.Fragment>
149
- );
150
- }
151
- ```
152
-
153
- The server-side rendering code becomes:
15
+ [&rArr; Documentation &lArr;](https://dr.pogodin.studio/docs/react-global-state/index.html)
154
16
 
155
- ```jsx
156
- /* Server-sider rendering. */
157
-
158
- import React from 'react';
159
- import ReactDOM from 'react-dom/server';
160
-
161
- import { GlobalStateProvider } from '@dr.pogodin/react-global-state';
162
-
163
- import SampleApp from 'path/to/app';
164
-
165
- // As you want to keep server latency within a reason, it is a good idea
166
- // to limit the maximum number of SSR rounds, or the maximum SSR time, or
167
- // both, and perform any async operations which took too long at the client
168
- // side.
169
- const MAX_SSR_ROUNDS = 3;
170
- const SSR_TIMEOUT = 1000; // 1 second (in milliseconds).
171
-
172
- async function renderServerSide() {
173
- let render;
174
- const start = Date.now();
175
- const ssrContext = { state: {} };
176
- for (let round = 0; round < MAX_SSR_ROUNDS; round += 1) {
177
- // SSR round.
178
- render = ReactDOM.renderToString((
179
- <GlobalStateProvider
180
- initialState={ssrContext.state}
181
- ssrContext={ssrContext}
182
- >
183
- <SampleApp />
184
- </GlobalStateProvider>
185
- ));
186
-
187
- // SSR round did not altered the global state: we are done.
188
- if (!ssrContext.dirty) break;
189
-
190
- // Waiting for pending async operations to complete.
191
- const timeout = SSR_TIMEOUT + start - Date.now();
192
- const ok = timeout > 0 && await Promise.race([
193
- Promise.allSettled(ssrContext.pending),
194
- new Promise((x) => setTimeout(() => x(false), timeout)),
195
- ]);
196
- if (!ok) break; // SSR timeout.
197
- }
198
- return { render, state: ssrContext.state };
199
- }
200
- ```
201
- &uArr; When `ssrContext` property is passed into the `<GlobalStateProvider>`,
202
- the corresponding global state object switches into the SSR mode. In this mode,
203
- if the app rendering modifies the state, the `ssrContext.dirty` flag is set
204
- `true`, and for any async operations, triggered by the library hooks,
205
- corresponding promises are added into the `ssrContext.pending` array.
206
- Thus, the block of code
207
- ```js
208
- if (!ssrContext.dirty) break;
209
-
210
- const timeout = SSR_TIMEOUT + start - Date.now();
211
- const ok = timeout > 0 && await Promise.race([
212
- Promise.allSettled(ssrContext.pending),
213
- new Promise((x) => setTimeout(() => x(false), timeout)),
214
- ]);
215
- if (!ok) break;
216
- ```
217
- in the case when last rendering pass triggered async operations, it waits
218
- for them to complete, and allows the rendering pass to be redone
219
- with the new initial value of the global state, which is written to
220
- `ssrContext.state` in this case. If no updates to the state happened
221
- in the last rendering pass, this block breaks out of the loop, leaving
222
- to you in the `render` variable the HTML markup to send to the client,
223
- and in the `ssrContext.state` the initial value of the global state to use
224
- for app initialization at the client side.
225
-
226
- The outer `for` loop serves to protect against possible long re-rendering
227
- loops: if after several re-renders the state has not become stable, it is
228
- fine to send to the client side the latest render and state results, and
229
- finalize the rest of rendering at the client side. Similarly, the timeout
230
- condition interrupts SSR and cause the code to serve the current render
231
- and state, if any pending async operation takes too long, thus compromising
232
- server latency.
233
-
234
- In case when some async operations are too long to wait for them during SSR,
235
- the async hooks accept `noSSR` option, to be ignored during SSR. Additional
236
- option is to wrap the rendering cycle into a timeout race codition, and if
237
- the desired rendering time has bit hit, the rendering loop can be interrupted,
238
- and the latest render and state can be sent to the client side.
239
-
240
- The corresponding client-side rendering is simple, just pass the state
241
- calculated during the server-side rendering into the `initialState` prop
242
- of `<GlobalStateProvider>` at the client side:
243
-
244
- ```jsx
245
- /* Client-side rendering. */
246
-
247
- import React from 'react';
248
- import ReactDOM from 'react-dom';
249
-
250
- import { GlobalStateProvider } from '@dr.pogodin/react-global-state';
251
-
252
- import SampleApp from 'path/to/app';
253
-
254
- function renderClientSide(stateFromServerSide) {
255
- ReactDOM.hydrate((
256
- <GlobalStateProvider initialState={stateFromServerSide}>
257
- <SampleApp />
258
- </GlobalStateProvider>
259
- ), document.getElementById('your-react-view'));
260
- }
261
- ```
262
-
263
- ### Frequently Asked Questions
264
-
265
- - _Does React Global State library avoid unnecessary component re-renders when values updated in the global state are irrelevant to those components?_
266
-
267
- Yes, it does avoid unnecessary re-renders of the component tree. A component
268
- relying on `some.path` in the global state is re-rendered only when the value
269
- at this path, or its sub-path has changed; _i.e._ it will be re-rendered if
270
- the value at `some.path` has changed, and it will be re-rendered if the value
271
- at `some.path.sub.path` has changed.
272
-
273
- - _How would you describe your use case compared to another React global state library, e.g. [Valtio](https://www.npmjs.com/package/valtio)?_
274
-
275
- 1. React Global State is designed to follow the standard React API as close
276
- as possible. _E.g._ if some component relies on the local state:
277
- ```jsx
278
- const [value, setValue] = useState(initialState);
279
- ```
280
- to move that value to the global state (or _vice versa_) one only needs to
281
- replace the hook with
282
- ```jsx
283
- const [value, setValue] = useGlobalState(path, initialState);
284
- ```
285
- The [useGlobalState()] hook takes care to follow all edge cases of the
286
- standard [useState()]: `setValue` setter identity is stable (does not
287
- change on re-renders), functional updates and lazy initial state are
288
- supported.
289
-
290
- Other libraries tend to re-invent the wheel, introducing their own APIs,
291
- which (i) should be learned and understood; (ii) do complicate migration
292
- of components between the local and global state, should it be needed in
293
- a course of app development / prototyping.
294
-
295
- 2. When it comes to async data in the global state other libraries tend to
296
- offer only a very basic supported, often relying on experimental or internal
297
- React mechanics.
298
-
299
- React Global State, [useAsyncData()] and [useAsyncCollection()] hooks in
300
- particular, implements async data fetching and management features: when
301
- multiple components use these hooks to load async data to the same global
302
- state path the library takes care to do the actual loading just once, and
303
- then keep the data without reloading until their age reaches (configurable)
304
- max age. There is an automated garbage collection of expired, non-used
305
- async data from the global state; there is server-side rendering (SSR)
306
- support, with suggested high-level setup taking care that all async data
307
- loaded using [useAsyncData()] and [useAsyncCollection()] hooks will be
308
- automatically loaded and used in server-side renders (still allowing to
309
- opt-out of that for individual hooks, and timeout server-side fetching of
310
- data that take too long to arrive, in which case the library will fetch
311
- such data client-side). It does not rely on experimental React APIs to
312
- achieve its functionality, it only uses current public APIs.
313
-
314
- For me the support of async data fetching into the global state and their
315
- further management with out-of-the-box SSR support was the primary
316
- motivation to create React Global State. There are many other global state
317
- React libraries, but I was not able to find any that would cover the async
318
- data handling with that ease I believed was possible. The secondary
319
- motivation was that existing global state libraries either had
320
- the shortcoming of unnecessary component re-renders when data irrelevant
321
- to them where updated in the global state, or introduced their
322
- own APIs, where following the standard React APIs for local state looks
323
- to me a way more convenient approach.
324
-
325
- - _Is React Global State library production ready (considering the current version number 0.y.z)?_
326
-
327
- Yes. I personally use it in production for all my commercial and personal
328
- React projects for over an year. I just don't feel like to call it v1 until
329
- a reasonable adoption by 3rd party developers, and any API improvements that
330
- may come out of community experience.
331
-
332
- [Library Reference](https://dr.pogodin.studio/docs/react-global-state/index.html) &bull;
333
- [Blog Article](https://dr.pogodin.studio/dev-blog/the-global-state-in-react-designed-right)
334
-
335
- [useAsyncCollection()]: https://dr.pogodin.studio/docs/react-global-state/docs/api/hooks/useasynccollection
336
- [useAsyncData()]: https://dr.pogodin.studio/docs/react-global-state/docs/api/hooks/useasyncdata
337
- [useGlobalState()]: https://dr.pogodin.studio/docs/react-global-state/docs/api/hooks/useglobalstate
338
- [useState()]: https://reactjs.org/docs/hooks-reference.html#usestate
339
-
340
- [functional updates]: https://reactjs.org/docs/hooks-reference.html#functional-updates
341
- [lazy initial state]: https://reactjs.org/docs/hooks-reference.html#functional-updates
17
+ [![Sponsor](https://raw.githubusercontent.com/birdofpreyru/react-global-state/master/.README/sponsor.svg)](https://github.com/sponsors/birdofpreyru)
@@ -0,0 +1,219 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _lodash = require("lodash");
8
+ var _utils = require("./utils");
9
+ const ERR_NO_SSR_WATCH = 'GlobalState must not be watched at server side';
10
+ class GlobalState {
11
+ #initialState;
12
+
13
+ // TODO: It is tempting to replace watchers here by
14
+ // Emitter from @dr.pogodin/js-utils, but we need to clone
15
+ // current watchers for emitting later, and this is not something
16
+ // Emitter supports right now.
17
+ #watchers = [];
18
+ #nextNotifierId;
19
+ #currentState;
20
+
21
+ /**
22
+ * Creates a new global state object.
23
+ * @param initialState Intial global state content.
24
+ * @param ssrContext Server-side rendering context.
25
+ */
26
+ constructor(initialState, ssrContext) {
27
+ this.#currentState = initialState;
28
+ this.#initialState = initialState;
29
+ if (ssrContext) {
30
+ /* eslint-disable no-param-reassign */
31
+ ssrContext.dirty = false;
32
+ ssrContext.pending = [];
33
+ ssrContext.state = this.#currentState;
34
+ /* eslint-enable no-param-reassign */
35
+
36
+ this.ssrContext = ssrContext;
37
+ }
38
+ if (process.env.NODE_ENV !== 'production' && (0, _utils.isDebugMode)()) {
39
+ /* eslint-disable no-console */
40
+ let msg = 'New ReactGlobalState created';
41
+ if (ssrContext) msg += ' (SSR mode)';
42
+ console.groupCollapsed(msg);
43
+ console.log('Initial state:', (0, _lodash.cloneDeep)(initialState));
44
+ console.groupEnd();
45
+ /* eslint-enable no-console */
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Gets entire state, the same way as .get(null, opts) would do.
51
+ * @param opts.initialState
52
+ * @param opts.initialValue
53
+ */
54
+ getEntireState(opts) {
55
+ let state = opts?.initialState ? this.#initialState : this.#currentState;
56
+ if (state !== undefined || opts?.initialValue === undefined) return state;
57
+ const iv = opts.initialValue;
58
+ state = (0, _lodash.isFunction)(iv) ? iv() : iv;
59
+ if (this.#currentState === undefined) this.setEntireState(state);
60
+ return state;
61
+ }
62
+
63
+ /**
64
+ * Notifies all connected state watchers that a state update has happened.
65
+ */
66
+ notifyStateUpdate(path, value) {
67
+ if (process.env.NODE_ENV !== 'production' && (0, _utils.isDebugMode)()) {
68
+ /* eslint-disable no-console */
69
+ const p = typeof path === 'string' ? `"${path}"` : 'none (entire state update)';
70
+ console.groupCollapsed(`ReactGlobalState update. Path: ${p}`);
71
+ console.log('New value:', (0, _lodash.cloneDeep)(value));
72
+ console.log('New state:', (0, _lodash.cloneDeep)(this.#currentState));
73
+ console.groupEnd();
74
+ /* eslint-enable no-console */
75
+ }
76
+
77
+ if (this.ssrContext) {
78
+ this.ssrContext.dirty = true;
79
+ this.ssrContext.state = this.#currentState;
80
+ } else if (!this.#nextNotifierId) {
81
+ this.#nextNotifierId = setTimeout(() => {
82
+ this.#nextNotifierId = undefined;
83
+ const watchers = [...this.#watchers];
84
+ for (let i = 0; i < watchers.length; ++i) {
85
+ watchers[i]();
86
+ }
87
+ });
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Sets entire state, the same way as .set(null, value) would do.
93
+ * @param value
94
+ */
95
+ setEntireState(value) {
96
+ if (this.#currentState !== value) {
97
+ this.#currentState = value;
98
+ this.notifyStateUpdate(null, value);
99
+ }
100
+ return value;
101
+ }
102
+
103
+ /**
104
+ * Gets current or initial value at the specified "path" of the global state.
105
+ * @param path Dot-delimitered state path.
106
+ * @param options Additional options.
107
+ * @param options.initialState If "true" the value will be read
108
+ * from the initial state instead of the current one.
109
+ * @param options.initialValue If the value read from the "path" is
110
+ * "undefined", this "initialValue" will be returned instead. In such case
111
+ * "initialValue" will also be written to the "path" of the current global
112
+ * state (no matter "initialState" flag), if "undefined" is stored there.
113
+ * @return Retrieved value.
114
+ */
115
+
116
+ // .get() without arguments just falls back to .getEntireState().
117
+ // This variant attempts to automatically resolve and check the type of value
118
+ // at the given path, as precise as the actual state and path types permit.
119
+ // If the automatic path resolution is not possible, the ValueT fallsback
120
+ // to `never` (or to `undefined` in some cases), effectively forbidding
121
+ // to use this .get() variant.
122
+ // This variant is not callable by default (without generic arguments),
123
+ // otherwise it allows to set the correct ValueT directly.
124
+ get(path, opts) {
125
+ if ((0, _lodash.isNil)(path)) {
126
+ const res = this.getEntireState(opts);
127
+ return res;
128
+ }
129
+ const state = opts?.initialState ? this.#initialState : this.#currentState;
130
+ let res = (0, _lodash.get)(state, path);
131
+ if (res !== undefined || opts?.initialValue === undefined) return res;
132
+ const iv = opts.initialValue;
133
+ res = (0, _lodash.isFunction)(iv) ? iv() : iv;
134
+ if (!opts?.initialState || this.get(path) === undefined) {
135
+ this.set(path, res);
136
+ }
137
+ return res;
138
+ }
139
+
140
+ /**
141
+ * Writes the `value` to given global state `path`.
142
+ * @param path Dot-delimitered state path. If not given, entire
143
+ * global state content is replaced by the `value`.
144
+ * @param value The value.
145
+ * @return Given `value` itself.
146
+ */
147
+
148
+ // This variant attempts automatic value type resolution & checking.
149
+ // This variant is disabled by default, otherwise allows to give
150
+ // expected value type explicitly.
151
+ set(path, value) {
152
+ if ((0, _lodash.isNil)(path)) return this.setEntireState(value);
153
+ if (value !== this.get(path)) {
154
+ const root = {
155
+ state: this.#currentState
156
+ };
157
+ let segIdx = 0;
158
+ let pos = root;
159
+ const pathSegments = (0, _lodash.toPath)(`state.${path}`);
160
+ for (; segIdx < pathSegments.length - 1; segIdx += 1) {
161
+ const seg = pathSegments[segIdx];
162
+ const next = pos[seg];
163
+ if (Array.isArray(next)) pos[seg] = [...next];else if ((0, _lodash.isObject)(next)) pos[seg] = {
164
+ ...next
165
+ };else {
166
+ // We arrived to a state sub-segment, where the remaining part of
167
+ // the update path does not exist yet. We rely on lodash's set()
168
+ // function to create the remaining path, and set the value.
169
+ (0, _lodash.set)(pos, pathSegments.slice(segIdx), value);
170
+ break;
171
+ }
172
+ pos = pos[seg];
173
+ }
174
+ if (segIdx === pathSegments.length - 1) {
175
+ pos[pathSegments[segIdx]] = value;
176
+ }
177
+ this.#currentState = root.state;
178
+ this.notifyStateUpdate(path, value);
179
+ }
180
+ return value;
181
+ }
182
+
183
+ /**
184
+ * Unsubscribes `callback` from watching state updates; no operation if
185
+ * `callback` is not subscribed to the state updates.
186
+ * @param callback
187
+ * @throws if {@link SsrContext} is attached to the state instance: the state
188
+ * watching functionality is intended for client-side (non-SSR) only.
189
+ */
190
+ unWatch(callback) {
191
+ if (this.ssrContext) throw new Error(ERR_NO_SSR_WATCH);
192
+ const watchers = this.#watchers;
193
+ const pos = watchers.indexOf(callback);
194
+ if (pos >= 0) {
195
+ watchers[pos] = watchers[watchers.length - 1];
196
+ watchers.pop();
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Subscribes `callback` to watch state updates; no operation if
202
+ * `callback` is already subscribed to this state instance.
203
+ * @param callback It will be called without any arguments every
204
+ * time the state content changes (note, howhever, separate state updates can
205
+ * be applied to the state at once, and watching callbacks will be called once
206
+ * after such bulk update).
207
+ * @throws if {@link SsrContext} is attached to the state instance: the state
208
+ * watching functionality is intended for client-side (non-SSR) only.
209
+ */
210
+ watch(callback) {
211
+ if (this.ssrContext) throw new Error(ERR_NO_SSR_WATCH);
212
+ const watchers = this.#watchers;
213
+ if (watchers.indexOf(callback) < 0) {
214
+ watchers.push(callback);
215
+ }
216
+ }
217
+ }
218
+ exports.default = GlobalState;
219
+ //# sourceMappingURL=GlobalState.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GlobalState.js","names":["_lodash","require","_utils","ERR_NO_SSR_WATCH","GlobalState","initialState","watchers","nextNotifierId","currentState","constructor","ssrContext","dirty","pending","state","process","env","NODE_ENV","isDebugMode","msg","console","groupCollapsed","log","cloneDeep","groupEnd","getEntireState","opts","undefined","initialValue","iv","isFunction","setEntireState","notifyStateUpdate","path","value","p","setTimeout","i","length","get","isNil","res","set","root","segIdx","pos","pathSegments","toPath","seg","next","Array","isArray","isObject","slice","unWatch","callback","Error","indexOf","pop","watch","push","exports","default"],"sources":["../../src/GlobalState.ts"],"sourcesContent":["import {\n cloneDeep,\n get,\n isFunction,\n isObject,\n isNil,\n set,\n toPath,\n} from 'lodash';\n\nimport SsrContext from './SsrContext';\n\nimport {\n type CallbackT,\n type ForceT,\n type TypeLock,\n type ValueAtPathT,\n type ValueOrInitializerT,\n isDebugMode,\n} from './utils';\n\nconst ERR_NO_SSR_WATCH = 'GlobalState must not be watched at server side';\n\ntype GetOptsT<T> = {\n initialState?: boolean;\n initialValue?: ValueOrInitializerT<T>;\n};\n\nexport default class GlobalState<\n StateT,\n SsrContextT extends SsrContext<StateT> = SsrContext<StateT>,\n> {\n readonly ssrContext?: SsrContextT;\n\n #initialState: StateT;\n\n // TODO: It is tempting to replace watchers here by\n // Emitter from @dr.pogodin/js-utils, but we need to clone\n // current watchers for emitting later, and this is not something\n // Emitter supports right now.\n #watchers: CallbackT[] = [];\n\n #nextNotifierId?: NodeJS.Timeout;\n\n #currentState: StateT;\n\n /**\n * Creates a new global state object.\n * @param initialState Intial global state content.\n * @param ssrContext Server-side rendering context.\n */\n constructor(\n initialState: StateT,\n ssrContext?: SsrContextT,\n ) {\n this.#currentState = initialState;\n this.#initialState = initialState;\n\n if (ssrContext) {\n /* eslint-disable no-param-reassign */\n ssrContext.dirty = false;\n ssrContext.pending = [];\n ssrContext.state = this.#currentState;\n /* eslint-enable no-param-reassign */\n\n this.ssrContext = ssrContext;\n }\n\n if (process.env.NODE_ENV !== 'production' && isDebugMode()) {\n /* eslint-disable no-console */\n let msg = 'New ReactGlobalState created';\n if (ssrContext) msg += ' (SSR mode)';\n console.groupCollapsed(msg);\n console.log('Initial state:', cloneDeep(initialState));\n console.groupEnd();\n /* eslint-enable no-console */\n }\n }\n\n /**\n * Gets entire state, the same way as .get(null, opts) would do.\n * @param opts.initialState\n * @param opts.initialValue\n */\n getEntireState(opts?: GetOptsT<StateT>): StateT {\n let state = opts?.initialState ? this.#initialState : this.#currentState;\n if (state !== undefined || opts?.initialValue === undefined) return state;\n\n const iv = opts.initialValue;\n state = isFunction(iv) ? iv() : iv;\n if (this.#currentState === undefined) this.setEntireState(state);\n return state;\n }\n\n /**\n * Notifies all connected state watchers that a state update has happened.\n */\n private notifyStateUpdate(path: null | string | undefined, value: unknown) {\n if (process.env.NODE_ENV !== 'production' && isDebugMode()) {\n /* eslint-disable no-console */\n const p = typeof path === 'string'\n ? `\"${path}\"` : 'none (entire state update)';\n console.groupCollapsed(`ReactGlobalState update. Path: ${p}`);\n console.log('New value:', cloneDeep(value));\n console.log('New state:', cloneDeep(this.#currentState));\n console.groupEnd();\n /* eslint-enable no-console */\n }\n\n if (this.ssrContext) {\n this.ssrContext.dirty = true;\n this.ssrContext.state = this.#currentState;\n } else if (!this.#nextNotifierId) {\n this.#nextNotifierId = setTimeout(() => {\n this.#nextNotifierId = undefined;\n const watchers = [...this.#watchers];\n for (let i = 0; i < watchers.length; ++i) {\n watchers[i]();\n }\n });\n }\n }\n\n /**\n * Sets entire state, the same way as .set(null, value) would do.\n * @param value\n */\n setEntireState(value: StateT): StateT {\n if (this.#currentState !== value) {\n this.#currentState = value;\n this.notifyStateUpdate(null, value);\n }\n return value;\n }\n\n /**\n * Gets current or initial value at the specified \"path\" of the global state.\n * @param path Dot-delimitered state path.\n * @param options Additional options.\n * @param options.initialState If \"true\" the value will be read\n * from the initial state instead of the current one.\n * @param options.initialValue If the value read from the \"path\" is\n * \"undefined\", this \"initialValue\" will be returned instead. In such case\n * \"initialValue\" will also be written to the \"path\" of the current global\n * state (no matter \"initialState\" flag), if \"undefined\" is stored there.\n * @return Retrieved value.\n */\n\n // .get() without arguments just falls back to .getEntireState().\n get(): StateT;\n\n // This variant attempts to automatically resolve and check the type of value\n // at the given path, as precise as the actual state and path types permit.\n // If the automatic path resolution is not possible, the ValueT fallsback\n // to `never` (or to `undefined` in some cases), effectively forbidding\n // to use this .get() variant.\n get<\n PathT extends null | string | undefined,\n ValueArgT extends ValueAtPathT<StateT, PathT, never>,\n ValueResT extends ValueAtPathT<StateT, PathT, void>,\n >(path: PathT, opts?: GetOptsT<ValueArgT>): ValueResT;\n\n // This variant is not callable by default (without generic arguments),\n // otherwise it allows to set the correct ValueT directly.\n get<Forced extends ForceT | false = false, ValueT = void>(\n path?: null | string,\n opts?: GetOptsT<TypeLock<Forced, never, ValueT>>,\n ): TypeLock<Forced, void, ValueT>;\n\n get<ValueT>(path?: null | string, opts?: GetOptsT<ValueT>): ValueT {\n if (isNil(path)) {\n const res = this.getEntireState((opts as unknown) as GetOptsT<StateT>);\n return (res as unknown) as ValueT;\n }\n\n const state = opts?.initialState ? this.#initialState : this.#currentState;\n\n let res = get(state, path);\n if (res !== undefined || opts?.initialValue === undefined) return res;\n\n const iv = opts.initialValue;\n res = isFunction(iv) ? iv() : iv;\n\n if (!opts?.initialState || this.get(path) === undefined) {\n this.set<ForceT, unknown>(path, res);\n }\n\n return res;\n }\n\n /**\n * Writes the `value` to given global state `path`.\n * @param path Dot-delimitered state path. If not given, entire\n * global state content is replaced by the `value`.\n * @param value The value.\n * @return Given `value` itself.\n */\n\n // This variant attempts automatic value type resolution & checking.\n set<\n PathT extends null | string | undefined,\n ValueArgT extends ValueAtPathT<StateT, PathT, never>,\n ValueResT extends ValueAtPathT<StateT, PathT, void>,\n >(path: PathT, value: ValueArgT): ValueResT;\n\n // This variant is disabled by default, otherwise allows to give\n // expected value type explicitly.\n set<Forced extends ForceT | false = false, ValueT = never>(\n path: null | string | undefined,\n value: TypeLock<Forced, never, ValueT>,\n ): TypeLock<Forced, void, ValueT>;\n\n set(path: null | string | undefined, value: unknown): unknown {\n if (isNil(path)) return this.setEntireState(value as StateT);\n\n if (value !== this.get(path)) {\n const root = { state: this.#currentState };\n let segIdx = 0;\n let pos: any = root;\n const pathSegments = toPath(`state.${path}`);\n for (; segIdx < pathSegments.length - 1; segIdx += 1) {\n const seg = pathSegments[segIdx];\n const next = pos[seg];\n if (Array.isArray(next)) pos[seg] = [...next];\n else if (isObject(next)) pos[seg] = { ...next };\n else {\n // We arrived to a state sub-segment, where the remaining part of\n // the update path does not exist yet. We rely on lodash's set()\n // function to create the remaining path, and set the value.\n set(pos, pathSegments.slice(segIdx), value);\n break;\n }\n pos = pos[seg];\n }\n\n if (segIdx === pathSegments.length - 1) {\n pos[pathSegments[segIdx]] = value;\n }\n\n this.#currentState = root.state;\n\n this.notifyStateUpdate(path, value);\n }\n return value;\n }\n\n /**\n * Unsubscribes `callback` from watching state updates; no operation if\n * `callback` is not subscribed to the state updates.\n * @param callback\n * @throws if {@link SsrContext} is attached to the state instance: the state\n * watching functionality is intended for client-side (non-SSR) only.\n */\n unWatch(callback: CallbackT) {\n if (this.ssrContext) throw new Error(ERR_NO_SSR_WATCH);\n\n const watchers = this.#watchers;\n const pos = watchers.indexOf(callback);\n if (pos >= 0) {\n watchers[pos] = watchers[watchers.length - 1];\n watchers.pop();\n }\n }\n\n /**\n * Subscribes `callback` to watch state updates; no operation if\n * `callback` is already subscribed to this state instance.\n * @param callback It will be called without any arguments every\n * time the state content changes (note, howhever, separate state updates can\n * be applied to the state at once, and watching callbacks will be called once\n * after such bulk update).\n * @throws if {@link SsrContext} is attached to the state instance: the state\n * watching functionality is intended for client-side (non-SSR) only.\n */\n watch(callback: CallbackT) {\n if (this.ssrContext) throw new Error(ERR_NO_SSR_WATCH);\n\n const watchers = this.#watchers;\n if (watchers.indexOf(callback) < 0) {\n watchers.push(callback);\n }\n }\n}\n"],"mappings":"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"}