react-statepod 0.4.0

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Alexander Tkačenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,514 @@
1
+ # react-statepod
2
+
3
+ A shared state management and routing lib for React apps. Under the hood, routing is shared state management, too, with the shared data being the URL.
4
+
5
+ <details>
6
+ <summary>Why think of another state management lib and how it compares to others</summary><br>
7
+
8
+ With several options available, state management in React apps still feels more cumbersome than it could be. With the React's local state mental model in mind, we might expect that a shared state management lib—
9
+
10
+ (1) has a simple API introducing a minimal set of entities,<br>
11
+ (2) requires minimal changes to move local state to shared state,<br>
12
+ (3) straightforwardly supports SSR without workaround APIs.
13
+
14
+ The popular approaches to state management seem to depart from at least one of these points:
15
+
16
+ Apparently focusing on other aspects, **Redux Toolkit** and **MobX** don't fulfill any of the points listed above by bringing in their own mental models of shared state management with an inventory of new concepts and approaches.
17
+
18
+ **Zustand** does have a simple and minimalist API (point (1) fulfilled), but it's not quite similar to the React's state API, which leads to significant code rewrites while migrating from local state to shared state (so simplicity alone is not sufficient, point (2) unfulfilled). The Zustand's SSR setup requires that the default React store setup be transitioned to vanilla stores<sup>[[1](https://github.com/pmndrs/zustand/tree/main#react-context)]</sup>, which adds another state management pattern to the code and yet another sizable migration rewrite (point (3) unmet). Also, subjectively, Zustand introduces patterns that look unidiomatic in React, like calling methods on Zustand's store hooks (`useXStore.subscribe()`)<sup>[[2](https://github.com/pmndrs/zustand/tree/main#transient-updates-for-often-occurring-state-changes)]</sup>.
19
+
20
+ With a useState-like API, **Jotai** is the closest match (points (1) and (2) fulfilled). Still, Jotai requires a workaround API, a special hook, to set up SSR<sup>[[3](https://jotai.org/docs/utilities/ssr)]</sup> (point (3) unmet). Also, subjectively, the Jotai's core `atom()` function with its elaborate capabilities is an overkill for state management that looks hard to tree-shake.
21
+
22
+ The `useExternalState()` hook of `react-statepod` is an attempt to come up with a mostly self-explanatory lightweight useState-like approach to shared state management by focusing on the three points listed above. The lib's other hooks are built around the common practical use cases for `useExternalState()`.
23
+
24
+ </details>
25
+
26
+ ---
27
+
28
+ Contents: [useExternalState](#useexternalstate) · [useRoute](#useroute) · [useNavigationStart / useNavigationComplete](#usenavigationstart--usenavigationcomplete) · [useRouteState](#useroutestate) · [Type-safe routes](#type-safe-routes) · [useTransientState](#usetransientstate) · [Annotated examples](#annotated-examples) · [Internals](#internals)
29
+
30
+ ## useExternalState
31
+
32
+ This hook is focused on simplicity of both setting up shared state from scratch and migrating from local state. The equally common latter scenario is often missed out with commonly used approaches resulting in sizable code rewrites.
33
+
34
+ ### Shared state
35
+
36
+ Move local state to the full-fledged shared state with minimal paradigm shift and minimal code changes:
37
+
38
+ ```diff
39
+ + import { State, useExternalState } from "react-statepod";
40
+ +
41
+ + const counterState = new State(0);
42
+
43
+ const Counter = () => {
44
+ - const [counter, setCounter] = useState(0);
45
+ + const [counter, setCounter] = useExternalState(counterState);
46
+
47
+ const handleClick = () => setCounter((value) => value + 1);
48
+
49
+ return <button onClick={handleClick}>+ {counter}</button>;
50
+ };
51
+
52
+ const ResetButton = () => {
53
+ - const [, setCounter] = useState(0);
54
+ + const [, setCounter] = useExternalState(counterState);
55
+
56
+ const handleClick = () => setCounter(0);
57
+
58
+ return <button onClick={handleClick}>×</button>;
59
+ };
60
+
61
+ const App = () => <><Counter/>{" "}<ResetButton/></>;
62
+ ```
63
+
64
+ ### Sharing state via Context
65
+
66
+ With SSR, it's common practice to put shared values into React Context rather than module-level variables to avoid cross-request data sharing. The same applies to external state. Provide external state to multiple components via React Context like any data in a React app:
67
+
68
+ ```diff
69
+ - const counterState = new State(0);
70
+ + const AppContext = createContext(new State(0));
71
+ ```
72
+
73
+ ```diff
74
+ - const [counter, setCounter] = useExternalState(counterState);
75
+ + const [counter, setCounter] = useExternalState(useContext(AppContext));
76
+ ```
77
+
78
+ ```jsx
79
+ const App = () => (
80
+ <AppContext.Provider value={new State(42)}>
81
+ <PlusButton/>{" "}<Display/>
82
+ </AppContext.Provider>
83
+ );
84
+ ```
85
+
86
+ ⬥ Like any data in a React app, the external state can also be split across multiple instances of `State` and multiple Contexts to maintain clearer semantic boundaries and more targeted data update subscriptions.
87
+
88
+ ⬥ Note that updating the `State` value doesn't change the instance's reference sitting in the React Context and therefore doesn't cause updates of the entire Context. Only the components subscribed to updates of the particular `State` instance by means of `useExternalState(state)` will be notified to re-render.
89
+
90
+ ### Filtering state updates
91
+
92
+ ⬥ One way of reducing re-renders in response to state changes is having multiple tightly scoped `State` instances in the app instead of having a larger chunk of disparate data in a single `State`. Yet another way is using the optional render callback of `useExternalState(state, callback)` for more fine-grained control over component's re-renders within a state subscription:
93
+
94
+ ```js
95
+ const itemState = new State({/* A map of `<id>: <item>` */});
96
+
97
+ // Renders a specific item from `itemState`
98
+ const ItemCard = ({ id }) => {
99
+ const [items, setItems] = useExternalState(itemState, (render, { current, previous }) => {
100
+ // Assuming that the items have a `timestamp` property, re-render
101
+ // `ItemCard` only if the relevant item's `timestamp` has increased
102
+ if (current[id].timestamp > previous[id].timestamp) render();
103
+ });
104
+
105
+ // ...
106
+ };
107
+ ```
108
+
109
+ ⬥ Use the optional `false` parameter in `useExternalState(state, false)` to tell the hook not to subscribe the component to tracking the external state updates altogether. A use case for it is when a component makes use of the external state value setter without using the state value itself. The `false` parameter could have been used in the `ResetButton` in the first example above, but in many cases with lightweight component renders it might be unnecessary, since React automatically skips updating the DOM when there are no changes.
110
+
111
+ ⬥ Splitting the app data into multiple `State` instances and the `useExternalState()`'s render callback serve a similar purpose as state slices and selectors adopted by some state management libs to offer fine-grained control over re-renders. A subtle difference in these approaches is that the state splitting and the render callback are more imperative and explicit about the conditions of re-renders than the state slices and selectors.
112
+
113
+ ### Integration with Immer
114
+
115
+ Immer can be used with state setters returned from `useExternalState()` just the same way as [with `useState()`](https://immerjs.github.io/immer/example-setstate#usestate--immer) to facilitate deeply nested data changes.
116
+
117
+ ### Persistence across page reloads
118
+
119
+ Replace `State` with `PersistentState` as shown below to get the state data synced to the specified `key` in `localStorage` and restored on page reload. After a persistent state is created, use it with `useExternalState(state)` the same way as `State` instances.
120
+
121
+ ```js
122
+ import { PersistentState } from "react-statepod";
123
+
124
+ const counterState = new PersistentState(0, { key: "counter" });
125
+ ```
126
+
127
+ ⬥ Set `options.session` to `true` in `new PersistentState(value, options)` to use `sessionStorage`.
128
+
129
+ ⬥ Set `options.serialize()` and `options.deserialize()` to override the default data transform behavior, including filtering and rearranging the data (it's `JSON.stringify()` and `JSON.parse()` by default).
130
+
131
+ ⬥ Set up interaction with a custom storage by setting `{ read(), write(value)? }` as `options` in `new PersistentState(value, options)`.
132
+
133
+ ⬥ `PersistentState` skips interaction with the browser storage in non-browser environments, which makes it usable with SSR. One way to avoid mismatch errors while hydrating SSR content based on a persisent state restored in the browser is using client-side rendering detection utilities such as [`react-clientside`](https://www.npmjs.com/package/react-clientside).
134
+
135
+ ## useRoute
136
+
137
+ Use this hook for URL-based rendering and SPA navigation, which boil down to accessing and changing the current URL treated as shared state under the hood.
138
+
139
+ ### URL-based rendering
140
+
141
+ URL-based rendering with `at(url, x, y?)` shown below works similarly to conditional rendering with the ternary operator `atURL ? x : y`. It's equally applicable to props and components:
142
+
143
+ ```jsx
144
+ import { useRoute } from "react-statepod";
145
+
146
+ const App = () => {
147
+ const { at } = useRoute();
148
+
149
+ return (
150
+ <header className={at("/", "full", "compact")}>
151
+ <h1>App</h1>
152
+ </header>
153
+ {at("/", <Intro/>)}
154
+ {at(/^\/sections\/(?<id>\d+)\/?$/, ({ params }) => <Section id={params.id}/>)}
155
+ );
156
+ };
157
+ ```
158
+
159
+ ⬥ `params` in dynamic values (as in `({ params }) => <Section id={params.id}/>` above) contains the URL pattern's capturing groups.
160
+
161
+ ⬥ Use `at(url)` as a shorthand for `at(url, true, false)`. Example: `<Component isOpen={at("/")}/>`.
162
+
163
+ ⬥ By default, `useRoute` makes use of the browser's URL, if it's available. Otherwise, use `<RouteProvider href={url}>` to set a specific URL value. Common use cases: SSR and tests. A less common use case: custom routing behavior, including custom non-URL-based routing ([example](https://codesandbox.io/p/sandbox/tykt44?file=%252Fsrc%252FApp.tsx)).
164
+
165
+ ⬥ See also the [Type-safe routes](#type-safe-routes) section.
166
+
167
+ ⬥ Routing with `react-statepod` is based on the core idea behind all approaches to route-based rendering: conditional rendering based on the URL. Unlike component-, config-, or file-based approaches, the `react-statepod`'s imperative approach sticks to this core idea without additional abstraction layers and specific relations between routes (like layout nesting or parameter inheritance) offering full explicit control over route-based rendering.
168
+
169
+ ### SPA navigation
170
+
171
+ The shape of the SPA navigation API is largely aligned with the similar built-in browser APIs (but still compatible with SSR):
172
+
173
+ ```diff
174
+ + import { A, useRoute } from "react-statepod";
175
+
176
+ const UserNav = ({ signedIn }) => {
177
+ + const { route } = useRoute();
178
+
179
+ const handleClick = () => {
180
+ - window.location.href = signedIn ? "/profile" : "/login";
181
+ + route.href = signedIn ? "/profile" : "/login";
182
+ };
183
+
184
+ return (
185
+ <nav>
186
+ - <a href="/">Home</a>
187
+ + <A href="/">Home</A>
188
+ <button onClick={handleClick}>Profile</button>
189
+ </nav>
190
+ );
191
+ };
192
+ ```
193
+
194
+ ⬥ `<A>` and `<Area>` are the two kinds of SPA route link components available out of the box. They have the same props and semantics as the corresponding HTML link elements `<a>` and `<area>`.
195
+
196
+ ⬥ The `route` object returned from `useRoute()` exposes an API resembling the built-in APIs of `window.location` and `history` carried over to SPA navigation: `.assign(url)`, `.replace(url)`, `.reload()`, `.href`, `.pathname`, `.search`, `.hash`, `.back()`, `.forward()`, `.go(delta)`.
197
+
198
+ ⬥ `route.navigate(options)` combines and extends `route.assign(url)` and `route.replace(url)` serving as a handy drop-in replacement for the similar `window.location` methods:
199
+
200
+ ```js
201
+ route.navigate({ href: "/intro", history: "replace", scroll: "off" });
202
+ ```
203
+
204
+ ⬥ Tweak link components by adding a relevant combination of the optional `data-` props corresponding to the `options` of `route.navigate(options)`:
205
+
206
+ ```jsx
207
+ <A href="/intro">Intro</A>
208
+ <A href="/intro" data-history="replace">Intro</A>
209
+ <A href="/intro" data-scroll="off">Intro</A>
210
+ <A href="/intro" data-spa="off">Intro</A>
211
+ ```
212
+
213
+ Using HTML link attributes as SPA link component props makes link components easily interchangeable with HTML links and more familiar without prior knowledge.
214
+
215
+ ⬥ Link components also automatically receive the `data-active="true"` prop whenever their `href` prop matches the current URL, which can be used for additional styling.
216
+
217
+ ⬥ Use the optional `callback` parameter of `useRoute(callback?)` for more fine-grained control over the component rendering in response to URL changes. This callback receives the `render` function as a parameter that should be called at some point. Use cases for this render callback include, for example, activating animated view transitions ([example](https://codesandbox.io/p/sandbox/w4q95n?file=%252Fsrc%252FApp.tsx)) or (less likely in regular circumstances) skipping re-renders for certain URL changes.
218
+
219
+ ## useNavigationStart / useNavigationComplete
220
+
221
+ These hooks set up optional actions to be done before and after a SPA navigation occurs respectively. Such intermediate actions are also known as routing middleware.
222
+
223
+ Some common examples of what can be handled with the routing middleware include redirecting to another URL, preventing navigation with unsaved user input, setting the page title based on the current URL:
224
+
225
+ ```jsx
226
+ import { useNavigationComplete, useNavigationStart } from "react-statepod";
227
+
228
+ function setTitle({ href }) {
229
+ document.title = href === "/intro" ? "Intro" : "App";
230
+ }
231
+
232
+ const App = () => {
233
+ const { route } = useRoute();
234
+ const [hasUnsavedChanges, setUnsavedChanges] = useState(false);
235
+
236
+ const handleNavigationStart = useCallback(({ href }) => {
237
+ if (hasUnsavedChanges)
238
+ return false; // Preventing navigation
239
+
240
+ if (href === "/") {
241
+ route.href = "/intro"; // SPA redirection
242
+ return false;
243
+ }
244
+ }, [hasUnsavedChanges, route]);
245
+
246
+ useNavigationStart(handleNavigationStart);
247
+ useNavigationComplete(setTitle);
248
+
249
+ // ...
250
+ };
251
+ ```
252
+
253
+ ⬥ The object parameter of the hooks' callbacks has the shape of the `route.navigate()`'s options, including `href` and `referrer`, the navigation destination and initial URLs.
254
+
255
+ ⬥ The callback of both hooks is first called when the component gets mounted if the route is already in the navigation-complete state.
256
+
257
+ ## useRouteState
258
+
259
+ When it's necessary to put a portion of the app's state to the URL, use this hook to manage URL parameters as state in a `useState`-like manner. Use the React's state mental model and migrate from local state without major code rewrites:
260
+
261
+ ```diff
262
+ + import { useRouteState } from "react-statepod";
263
+
264
+ const App = () => {
265
+ - const [{ coords }, setState] = useState({ coords: { x: 0, y: 0 } });
266
+ + const [{ query }, setState] = useRouteState("/");
267
+
268
+ const setPosition = () => {
269
+ setState(state => ({
270
+ ...state,
271
+ - coords: {
272
+ + query: {
273
+ x: Math.random(),
274
+ y: Math.random(),
275
+ },
276
+ });
277
+ };
278
+
279
+ return (
280
+ <>
281
+ <h1>Shape</h1>
282
+ - <Shape x={coords.x} y={coords.y}/>
283
+ + <Shape x={query.x} y={query.y}/>
284
+ <p><button onClick={setPosition}>Move</button></p>
285
+ </>
286
+ );
287
+ };
288
+ ```
289
+
290
+ ⬥ `useRouteState(url, options?)` has an optional second parameter in the shape of the `route.navigate()`'s options. Pass `{ scroll: "off" }` as `options` to opt out from the default scroll-to-the-top behavior when the URL changes.
291
+
292
+ ⬥ See also the [Type-safe routes](#type-safe-routes) section.
293
+
294
+ ## Type-safe routes
295
+
296
+ When it comes to accessing parameters extracted from a URL pattern, by default the parameters are typed as `Record<string, string | undefined>`, which quite literally represents a map containing portions of a string URL.
297
+
298
+ ```tsx
299
+ const { at } = useRoute();
300
+
301
+ at(/^\/sections\/(?<id>\d+)\/?$/, ({ params }) => <Section id={params.id}/>)
302
+ // ^ Record<string, string | undefined>
303
+
304
+ const [state, setState] = useRouteState("/");
305
+ // ^ { query: Record<string, string | undefined> }
306
+ ```
307
+
308
+ Optionally, more specific type-aware parsing of URL parameters can be achieved by replacing string and RegExp URL patterns with URL patterns produced by a schema-based URL builder, like with `url-shape` and `zod` or a [similar tool](https://standardschema.dev/schema#what-schema-libraries-implement-the-spec):
309
+
310
+ ```ts
311
+ import { createURLBuilder } from "url-shape";
312
+ import { z } from "zod"; // Or another Standard Schema-compliant lib
313
+
314
+ // Get a type-aware URL builder `url()` based on a URL schema
315
+ export const url = createURLBuilder({
316
+ "/sections/:id": z.object({
317
+ // URL path placeholder parameters
318
+ params: z.object({ id: z.coerce.number() }),
319
+ }),
320
+ "/": z.object({
321
+ // URL query (or search) parameters
322
+ query: z.optional(z.object({ x: z.coerce.number(), y: z.coerce.number() })),
323
+ }),
324
+ });
325
+ ```
326
+
327
+ The type-aware URL builder `url(pattern, options?)` provides hints about the types of the parsed URL parameters and helps avoid typos and type mismatches:
328
+
329
+ ```tsx
330
+ const { at } = useRoute();
331
+
332
+ at(url("/sections/:id"), ({ params }) => <Section id={params.id}/>)
333
+ // ^ { id: number }
334
+
335
+ const [state, setState] = useRouteState(url("/"));
336
+ // ^ { query?: { x: number, y: number } }
337
+
338
+ <A href={url("/sections/:id", { id: 1 })}>Section 1</A>
339
+ // ^ { id: number }
340
+ ```
341
+
342
+ The URL schema as shown above doesn't have to cover the entire app. This approach allows for incremental or partial adoption of type-safe routing, where needed.
343
+
344
+ On the other hand, once the entire app is covered with type-safe routes, we might want to avoid future use of relaxed typing with string and RegExp URL patterns. This can be achieved by adding the following type declaration that effectively disallows string and RegExp URL patterns:
345
+
346
+ ```ts
347
+ declare module "react-statepod" {
348
+ interface URLConfig {
349
+ strict: true;
350
+ }
351
+ }
352
+ ```
353
+
354
+ ### Nested routes
355
+
356
+ All routes are handled independently, so type-safe nested routes don't require special handling and don't maintain implicit relations with their parent routes. It also means that nested routes don't inherit their parent route parameters by default. Relations between routes (also beyond the direct inheritance of parameters) can be pretty straightforwardly defined at the URL schema level without imposing implicit constraints, which could be hard to work around.
357
+
358
+ ```ts
359
+ import { createURLBuilder } from "url-shape";
360
+ import { z } from "zod";
361
+
362
+ const sectionParams = z.object({
363
+ sectionId: z.coerce.number(),
364
+ });
365
+
366
+ export const url = createURLBuilder({
367
+ "/sections/:sectionId": z.object({
368
+ params: sectionParams,
369
+ }),
370
+ "/sections/:sectionId/stories/:storyId": z.object({
371
+ params: z.object({
372
+ ...sectionParams.shape, // Shared URL parameters
373
+ storyId: z.coerce.number(),
374
+ }),
375
+ }),
376
+ });
377
+ ```
378
+
379
+ ## useTransientState
380
+
381
+ Use this hook to track an async action's state, whether it's pending, successfully completed, or failed, without affecting the application's data management.
382
+
383
+ In the example below, storing and rendering the essential app data (`items`) and the happy path scenario remain unaffected. The loading and error state handling works like a decoupled scaffolding to the main scenario. (`items` are stored in local state here, but any other state used by the app can be there instead.)
384
+
385
+ ```diff
386
+ + import { useTransientState } from "react-statepod";
387
+ - import { fetchItems } from "./fetchItems.js";
388
+ + import { fetchItems as fetchItemsOriginal } from "./fetchItems.js";
389
+
390
+ export const ItemList = () => {
391
+ const [items, setItems] = useState([]);
392
+ + const [state, fetchItems] = useTransientState("items", fetchItemsOriginal);
393
+
394
+ useEffect(() => {
395
+ // The fetched items can be stored with any approach to app state
396
+ fetchItems().then(setItems);
397
+ }, [fetchItems]);
398
+
399
+ + if (state.initial || state.pending) return <p>Loading...</p>;
400
+ + if (state.error) return <p>An error occurred</p>;
401
+
402
+ return <ul>{items.map(/* ... */)}</ul>;
403
+ };
404
+ ```
405
+
406
+ ```diff
407
+ + import { useTransientState } from "react-statepod";
408
+
409
+ - export const Status = ({ state }) => {
410
+ + export const Status = () => {
411
+ + const [state] = useTransientState("items");
412
+
413
+ if (state.initial) return null;
414
+ if (state.pending) return <>Busy</>;
415
+ if (state.error) return <>Error</>;
416
+
417
+ return <>OK</>;
418
+ };
419
+ ```
420
+
421
+ ### Shared and local async action state
422
+
423
+ Use a string key with `useTransientState(key, action?)` to access the same action state from multiple components (as in `ItemList` and `Status` above). Pass `null` as the key to have the action state scoped locally to the component where the hook is used.
424
+
425
+ ### Silent pending state
426
+
427
+ Use case: background actions or optimistic updates.
428
+
429
+ Set `{ silent: true }` as the last parameter of the trackable action returned from the `useTransientState` hook to prevent the `pending` property from switching to `true` in the pending state.
430
+
431
+ ```js
432
+ const [state, fetchItems] = useTransientState(fetchItemsOriginal);
433
+ // ^ `state.pending` remains `false` in the silent mode
434
+
435
+ fetchItems({ silent: true })
436
+ ```
437
+
438
+ ### Delayed pending state
439
+
440
+ Use case: Avoid flashing a process indicator when the action is likely to complete in a short while by delaying the pending state.
441
+
442
+ ```js
443
+ const [state, fetchItems] = useTransientState(fetchItemsOriginal);
444
+ // ^ `state.pending` remains `false` during the delay
445
+
446
+ fetchItems({ delay: 500 }) // in milliseconds
447
+ ```
448
+
449
+ ### Custom rejection handler
450
+
451
+ Allow the trackable action to reject explicitly with `{ throws: true }` as the last parameter, along with exposing `state.error` returned from `useTransientState` that goes by default.
452
+
453
+ ```js
454
+ const [state, fetchItems] = useTransientState(fetchItemsOriginal);
455
+
456
+ fetchItems({ throws: true }).catch(handleError)
457
+ ```
458
+
459
+ ### Action state provider
460
+
461
+ `<TransientStateProvider>` creates an isolated instance of initial shared async action state. Its prime use cases are SSR and tests. It isn't required with client-side rendering, but it can be used to separate action states of larger self-contained portions of an app.
462
+
463
+ ```jsx
464
+ import { TransientStateProvider } from "react-statepod";
465
+
466
+ <TransientStateProvider>
467
+ <App/>
468
+ </TransientStateProvider>
469
+ ```
470
+
471
+ Use the provider to set up a specific initial async action state when required:
472
+
473
+ ```jsx
474
+ const initialState = {
475
+ "fetch-items": { initial: false, pending: true },
476
+ };
477
+
478
+ <TransientStateProvider value={initialState}>
479
+ <App/>
480
+ </TransientStateProvider>
481
+ ```
482
+
483
+ ⬥ With an explicit value or without, the `<TransientStateProvider>`'s nested components will only respond to updates in the particular action state they subscribed to by means of `useTransientState`.
484
+
485
+ ## Annotated examples
486
+
487
+ Shared state
488
+
489
+ - [Shared state without Context](https://codesandbox.io/p/sandbox/gxkn85?file=%252Fsrc%252FApp.tsx), counter app, useExternalState
490
+ - [Shared state with Context](https://codesandbox.io/p/sandbox/9mpfsf?file=%252Fsrc%252FApp.tsx), counter app, useExternalState, React Context
491
+ - [Shared state with Immer](https://codesandbox.io/p/sandbox/gv4rgw?file=%252Fsrc%252FApp.tsx), counter app, useExternalState, Immer
492
+
493
+ Routing
494
+
495
+ - [URL-based rendering](https://codesandbox.io/p/sandbox/2nv8ck?file=%252Fsrc%252FApp.tsx), useRoute, link component
496
+ - [Type-safe URL-based rendering](https://codesandbox.io/p/sandbox/tltq5r?file=%252Fsrc%252FApp.tsx), useRoute, url-shape, zod
497
+ - [URL parameters as state](https://codesandbox.io/p/sandbox/6rp4sy?file=%252Fsrc%252FApp.tsx), useRouteState
498
+ - [Type-safe URL parameters as state](https://codesandbox.io/p/sandbox/6ck4qz?file=%252Fsrc%252FShapeSection.tsx), useRouteState, url-shape, zod
499
+ - [Type-safe nested routes](https://codesandbox.io/p/sandbox/pv9rgh?file=%252Fsrc%252FApp.tsx), useRoute, url-shape, zod
500
+ - [Unknown routes](https://codesandbox.io/p/sandbox/jnngqt?file=%252Fsrc%252FApp.tsx), useRoute
501
+ - [Lazy routes](https://codesandbox.io/p/sandbox/qw5r6g?file=%252Fsrc%252FApp.tsx), useRoute, React Suspense, React.lazy
502
+ - [View transitions](https://codesandbox.io/p/sandbox/w4q95n?file=%252Fsrc%252FApp.tsx), useRoute, View Transition API
503
+ - [Custom routing based on text input](https://codesandbox.io/p/sandbox/tykt44?file=%252Fsrc%252FApp.tsx), Route, RouteProvider, useRoute
504
+ - [Converting links in HTML content to SPA links](https://codesandbox.io/p/sandbox/7pfjc7?file=%252Fsrc%252FApp.tsx), useRouteLinks
505
+
506
+ Async action state
507
+
508
+ - [Shared async action state](https://codesandbox.io/p/sandbox/x9d2c9?file=%252Fsrc%252FItemList.tsx), useTransientState
509
+
510
+ Find also the code of these examples in the repo's [`tests`](https://github.com/axtk/react-statepod/tree/main/tests) directory.
511
+
512
+ ## Internals
513
+
514
+ [`statepod`](https://www.npmjs.com/package/statepod)