` respectively.
208 |
209 | Instead of passing a function, you now pass an array of path selections. The state returned will be an array of values per each state selection path. E.g:
210 |
211 | ```ts
212 | const [isDarkMode] = useStoreStateOpt(UIStore, [["isDarkMode"]]);
213 | ```
214 |
215 | The performance benefits stem from Pullstate not having to run equality checks on the results of your selected state and then re-render your component accordingly, but instead looks at the immer update patches directly for which paths changed in your state and re-renders the listeners on those paths.
216 |
217 | ## 1.1.0
218 |
219 | Fixed issue with postActionHook not being called on the server for Async Actions.
220 |
221 | Added the following methods on Async Actions:
222 |
223 | - `setCached()`
224 | - `updateCached()`
225 |
226 | For a more finer-grained control of async action cache.
227 |
228 | `updateCached()` functions exactly the same as `update()` on stores, except it only runs on a previously successfully returned cached value. If nothing is cached, nothing is run.
229 |
230 | ## 1.0.0-beta.7
231 |
232 | Replaced `shallowEqual` from `fbjs` with the tiny package `fast-deep-equal` for object comparisons in various parts of the lib.
233 |
234 | ## 1.0.0-beta.6
235 |
236 | Fixed the `postActionHook` to work correctly when hitting a cached value.
237 |
238 | ## 0.8.0.alpha-2
239 |
240 | Added `IPullstateInstanceConsumable` as an export to help people who want to create code using the Pullstate stores' instance.
241 |
242 | ## 0.8.0.alpha-1
243 |
244 | Some refactoring of the Async Actions and adding of hooks for much finer grained control:
245 |
246 | `shortCicuitHook()`: Run checks to resolve the action with a response before it even sets out.
247 |
248 | `breakCacheHook()`: When an action's state is being returned from the cache, this hook allows you to run checks on the current cache and your stores to decide whether this action should be run again (essentially flushing / breaking the cache).
249 |
250 | `postActionHook()`: This hook allows you to run some things after the action has resolved, and most importantly allows code to run after each time we hit the cached result of this action as well. This is very useful for interface changes which need to change / update outside of the action code.
251 |
252 | `postActionHook()` is run with a context variable which tells you in which context it was run, one of: CACHE, SHORT_CIRCUIT, DIRECT_RUN
253 |
254 | These hooks should hopefully allow even more boilerplate code to be eliminated while working in asynchronous state scenarios.
255 |
256 | ## 0.7.1
257 |
258 | - Made the `isResolved()` function safe from causing infinite loops (Async Action resolves, but the state of the store still makes `isResolved()` return false which causes a re-trigger when re-rendering - most likely happens when not checking for error states in `isResolved()`) - instead posting an error message to the console informing about the loop which needs to be fixed.
259 |
260 | ## 0.7.0
261 |
262 | **:warning: Replaced with async action hooks above in 0.8.0**
263 |
264 | Added the options of setting an `isResolve()` synchronous checking function on Async Actions. This allows for early escape hatching (we don't need to run this async action based on the current state) and cache busting (even though we ran this Async Action before and we have a cached result, the current state indicates we need to run it again).
265 |
266 | You can set it like so:
267 |
268 | ```typescript jsx
269 | const loadEntity = PullstateCore.createAsyncAction<{ id: string }>(
270 | async ({ id }, { EntityStore }) => {
271 | const resp = await endpoints.getEntity({ id });
272 |
273 | if (resp.positive) {
274 | EntityStore.update((s) => {
275 | s.viewingEntity = resp.payload;
276 | });
277 | return successResult();
278 | }
279 |
280 | return errorResult(resp.endTags, resp.endMessage);
281 | },
282 |
283 | // This second argument is the isResolved() function
284 |
285 | ({ id }, { EntityStore }) => {
286 | const { viewingEntity } = EntityStore.getRawState();
287 |
288 | if (viewingEntity !== null && viewingEntity.id === id) {
289 | return successResult();
290 | }
291 |
292 | return false;
293 | }
294 | );
295 | ```
296 |
297 | It has the same form as the regular Async Action function, injecting the arguments and the stores - but needs to return a synchronous result of either `false` or the expected end result (as if this function would have run asynchronously).
298 |
299 | ## 0.6.0
300 |
301 | - Added "reactions" to store state. Usable like so:
302 |
303 | ```typescript jsx
304 | UIStore.createReaction(
305 | (s) => s.valueToListenForChanges,
306 | (draft, original, watched) => {
307 | // do something here when s.valueToListenForChanges changes
308 | // alter draft as usual - like regular update()
309 | // watched = the value returned from the first function (the selector for what to watch)
310 | }
311 | );
312 | ```
313 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:8.11.4
2 |
3 | WORKDIR /app/website
4 |
5 | EXPOSE 3000 35729
6 | COPY ./docs /app/docs
7 | COPY ./website /app/website
8 | RUN yarn install
9 |
10 | CMD ["yarn", "start"]
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2018-present Paul Myburgh and contributors
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ### pullstate
6 |
7 | > Ridiculously simple state stores with performant retrieval anywhere
8 | > in your React tree using the wonderful concept of React hooks!
9 |
10 | * ~7KB minified and gzipped! (excluding Immer and React)
11 | * Built with Typescript, providing a great dev experience if you're using it too
12 | * Uses [immer](https://github.com/mweststrate/immer) for state updates - easily and safely mutate your state directly!
13 | * **NEW** - [Create async actions](https://lostpebble.github.io/pullstate/docs/async-actions-introduction) and use React hooks or ` ` to have complete control over their UI states!
14 |
15 | _Originally inspired by the now seemingly abandoned library - [bey](https://github.com/jamiebuilds/bey). Although substantially
16 | different now- with Server-side rendering and Async Actions built in! Bey was in turn inspired by
17 | [react-copy-write](https://github.com/aweary/react-copy-write)._
18 |
19 | Try out a quick example:
20 |
21 | [](https://codesandbox.io/s/myvj8zzypp)
22 |
23 | ### 🎉 **[New documentation site is live!](https://lostpebble.github.io/pullstate/)**
24 |
25 | * [Installation](https://lostpebble.github.io/pullstate/docs/installation)
26 | * [Quick example](https://lostpebble.github.io/pullstate/docs/quick-example)
27 | * [Quick example - Server rendering](https://lostpebble.github.io/pullstate/docs/quick-example-server-rendered)
28 | * [Async Actions](https://lostpebble.github.io/pullstate/docs/async-actions-introduction)
29 | * [Creation](https://lostpebble.github.io/pullstate/docs/async-actions-creating)
30 | * [Usage](https://lostpebble.github.io/pullstate/docs/async-action-use)
31 | * [Async action hooks](https://lostpebble.github.io/pullstate/docs/async-hooks-overview)
32 |
33 | ---
34 |
35 | # **Let's dive right in**
36 |
37 | > This is taken directly from [the documentation site](https://lostpebble.github.io/pullstate/docs/quick-example), to give you a quick overview of Pullstate here on github. Be sure to check out the site to learn more.
38 |
39 | To start off, install `pullstate`.
40 |
41 | ```bash
42 | yarn add pullstate
43 | ```
44 |
45 | ## Create a store
46 |
47 | Define the first **state store**, by passing an initial state to `new Store()`:
48 |
49 |
50 | ```jsx
51 | import { Store } from "pullstate";
52 |
53 | export const UIStore = new Store({
54 | isDarkMode: true,
55 | });
56 | ```
57 |
58 | ## Read our store's state
59 |
60 | Then, in React, we can start using the state of that store using a simple hook `useState()`:
61 |
62 | ```tsx
63 | import * as React from "react";
64 | import { UIStore } from "./UIStore";
65 |
66 | export const App = () => {
67 | const isDarkMode = UIStore.useState(s => s.isDarkMode);
68 |
69 | return (
70 |
75 |
Hello Pullstate
76 |
77 | );
78 | };
79 | ```
80 |
81 | The argument to `useState()` over here (`s => s.isDarkMode`), is a selection function that ensures we select only the state that we actually need for this component. This is a big performance booster, as we only listen for changes (and if changed, re-render the component) on the exact returned values - in this case, simply the value of `isDarkMode`.
82 |
83 | ---
84 |
85 | ## Add interaction (update state)
86 |
87 | Great, so we are able to pull our state from `UIStore` into our App. Now lets add some basic interaction with a ``:
88 |
89 | ```tsx
90 | return (
91 |
96 |
Hello Pullstate
97 |
99 | UIStore.update(s => {
100 | s.isDarkMode = !isDarkMode;
101 | })
102 | }>
103 | Toggle Dark Mode
104 |
105 |
106 | );
107 | ```
108 |
109 | Notice how we call `update()` on `UIStore`, inside which we directly mutate the store's state. This is all thanks to the power of `immer`, which you can check out [here](https://github.com/immerjs/immer).
110 |
111 | Another pattern, which helps to illustrate this further, would be to actually define the action of toggling dark mode to a function on its own:
112 |
113 |
114 | ```tsx
115 | function toggleMode(s) {
116 | s.isDarkMode = !s.isDarkMode;
117 | }
118 |
119 | // ...in our code
120 | UIStore.update(toggleMode)}>Toggle Dark Mode
121 | ```
122 |
123 | Basically, to update our app's state all we need to do is create a function (inline arrow function or regular) which takes the current store's state and mutates it to whatever we'd like the next state to be.
124 |
125 | ## Omnipresent state updating
126 |
127 | Something interesting to notice at this point is that we are just importing `UIStore` directly and running `update()` on it:
128 |
129 | ```tsx
130 | import { UIStore } from "./UIStore";
131 |
132 | // ...in our code
133 | UIStore.update(toggleMode)}>Toggle Dark Mode
134 | ```
135 |
136 | And our components are being updated accordingly. We have freed our app's state from the confines of the component! This is one of the main advantages of Pullstate - allowing us to separate our state concerns from being locked in at the component level and manage things easily at a more global level from which our components listen and react (through our `useStoreState()` hooks).
137 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 |
3 | services:
4 | docusaurus:
5 | build: .
6 | ports:
7 | - 3000:3000
8 | - 35729:35729
9 | volumes:
10 | - ./docs:/app/docs
11 | - ./website/blog:/app/website/blog
12 | - ./website/core:/app/website/core
13 | - ./website/i18n:/app/website/i18n
14 | - ./website/pages:/app/website/pages
15 | - ./website/static:/app/website/static
16 | - ./website/sidebars.json:/app/website/sidebars.json
17 | - ./website/siteConfig.js:/app/website/siteConfig.js
18 | working_dir: /app/website
19 |
--------------------------------------------------------------------------------
/docs/_removed.md:
--------------------------------------------------------------------------------
1 |
2 | ```tsx
3 | // UIStore.ts
4 | import { Store } from "pullstate";
5 |
6 | interface IUIStore {
7 | isDarkMode: boolean;
8 | }
9 |
10 | export const UIStore = new Store({
11 | isDarkMode: true,
12 | });
13 | ```
14 |
15 | Server-rendering requires that we create a central place to reference all our stores, and we do this using `createPullstateCore()`:
16 |
17 | ```tsx
18 | // PullstateCore.ts
19 | import { UIStore } from "./stores/UIStore";
20 | import { createPullstateCore } from "pullstate";
21 |
22 | export const PullstateCore = createPullstateCore({
23 | UIStore
24 | });
25 |
26 | ```
27 |
28 | ---
29 |
30 | For example:
31 |
32 | ```tsx
33 | // a useEffect() hook in our functional component
34 |
35 | useEffect(() => {
36 | const tileLayer = L.tileLayer(tileTemplate.url, {
37 | minZoom: 3,
38 | maxZoom: 18,
39 | }).addTo(mapRef.current);
40 |
41 | const unsubscribeFromTileTemplate = GISStore.createReaction(
42 | s => s.tileLayerTemplate,
43 | newTemplate => {
44 | tileLayer.setUrl(newTemplate.url);
45 | }
46 | );
47 |
48 | return () => {
49 | unsubscribeFromTileTemplate();
50 | };
51 | }, []);
52 | ```
53 |
54 | As you can see we receive a function back from `createReaction()` which we have used here in the "cleanup" return function of `useEffect()` to unsubscribe from this reaction.
55 |
--------------------------------------------------------------------------------
/docs/assets/async-flow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lostpebble/pullstate/2a4651a8e16f543b484298592cf894c281c98c16/docs/assets/async-flow.png
--------------------------------------------------------------------------------
/docs/async-action-use.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: async-action-use
3 | title: Ways to make use of Async Actions
4 | sidebar_label: Use Async Actions
5 | ---
6 |
7 | *For the sake of being complete in our examples, all possible return states are shown - in real application usage, you might only use a subset of these values.*
8 |
9 | **All examples make use of the previously created Async Action `searchPicturesForTag()`, you can [see more in action creation](async-actions-creating.md).**
10 |
11 | ## Watch an Async Action (React hook)
12 |
13 | ```ts
14 | const [started, finished, result, updating] = searchPicturesForTag.useWatch({ tag }, options);
15 | ```
16 |
17 | * This **React hook** "watches" the action. By watching we mean that we are not initiating this action, but only listening for when this action actually starts through some other means (tracked with `started` here), and then all its states after.
18 | * Possible action states (if `true`):
19 | * `started` : This action has begun its execution.
20 | * `finished`: This action has finished
21 | * `updating`: This is a special action state which can be instigated through `run()`, or when an update triggers and we had passed the option `holdPrevious: true`, which we will see further down.
22 | * `result` is the structured result object you return from your action ([see more in action creation](async-actions-creating.md#what-to-return-from-an-action)).
23 |
24 | `watch()` also takes an options object as the second argument.
25 |
26 | #### Options
27 |
28 | ```ts
29 | {
30 | postActionEnabled?: boolean;
31 | cacheBreakEnabled?: boolean;
32 | holdPrevious?: boolean;
33 | dormant?: boolean;
34 | }
35 | ```
36 |
37 | _(Explained in next paragraph)_
38 |
39 | ## Beckon an Async Action (React hook)
40 |
41 | ```tsx
42 | const [finished, result, updating] = searchPicturesForTag.useBeckon({ tag }, options);
43 | ```
44 |
45 | * Exactly the same as `useWatch()` above, except this time we instigate this action when this hook is first called.
46 |
47 | * Same action states, except for `started` since we are starting this action by default
48 |
49 | `beckon()` also takes an options object as the second argument.
50 |
51 | #### Options
52 |
53 | ```ts
54 | {
55 | postActionEnabled?: boolean;
56 | cacheBreakEnabled?: boolean;
57 | holdPrevious?: boolean;
58 | dormant?: boolean;
59 | ssr?: boolean;
60 | }
61 | ```
62 |
63 | * You can disable the `postActionHook` and / or `cacheBreakHook` for this interaction with this action by using the options here. See more about [`hooks`](async-hooks-overview.md).
64 |
65 | * `holdPrevious` is a special option that allows the result value from this calling of the Async Action to remain in place while we are currently executing the next set of arguments. (e.g. still displaying the previous search results while the system is querying for the next set)
66 |
67 | * `dormant` is a way by which you can basically make Async Actions conditional. If `dormant = true`, then this action will not listen / execute at all.
68 |
69 | ### Ignore `beckon()` for server-rendering
70 |
71 | * If you are server rendering and you would _not_ like a certain Async Action to be instigated on the server (i.e. you are fine with the action resolving itself client-side only), you can pass as an option to beckon `{ ssr: false }`.
72 |
73 | ## (React Suspense) Read an Async Action
74 |
75 | *You can read more about React Suspense on the [React website](https://reactjs.org/docs/concurrent-mode-suspense.html)*
76 |
77 | ```tsx
78 | const PicturesDisplay = ({ tag }) => {
79 | const pictures = searchPicturesForTag.read({ tag });
80 |
81 | // make use of the pictures data here as if it was regular, loaded state
82 | }
83 |
84 | const PicturesPage = () => {
85 | return (
86 | Loading Pictures....}>
87 |
88 |
89 | );
90 | }
91 | ```
92 |
93 | You can pass the following options to `read(args, options)`:
94 |
95 | ```ts
96 | interface Options {
97 | postActionEnabled?: boolean;
98 | cacheBreakEnabled?: boolean;
99 | }
100 | ```
101 |
102 | ## Run an Async Action directly
103 |
104 | ```tsx
105 | const result = await searchPicturesForTag.run({ tag });
106 | ```
107 |
108 | * Run's the async action directly, just like a regular promise. Any actions that are currently being watched by means of `useWatch()` will have `started = true` at this moment.
109 |
110 | The return value of `run()` is the action's result object. Generally it is unimportant, and `run()` is mostly used for initiating watched actions, or initiating updates.
111 |
112 | `run()` also takes an optional options object:
113 |
114 | ```jsx
115 | const result = await searchPicturesForTag.run({ tag }, options);
116 | ```
117 |
118 | The structure of the options:
119 |
120 | ```tsx
121 | interface Options {
122 | treatAsUpdate: boolean, // default = false
123 | respectCache: boolean, // default = false
124 | ignoreShortCircuit: boolean, // default = false
125 | }
126 | ```
127 |
128 | #### `treatAsUpdate`
129 |
130 | As seen in the hooks for `useWatch()` and `useBeckon()`, there is an extra return value called `updating` which will be set to `true` if these conditions are met:
131 |
132 | * The action is `run()` with `treatAsUpdate: true` passed as an option.
133 |
134 | * The action has previously completed
135 |
136 | If these conditions are met, then `finished` shall remain `true`, and the current cached result unchanged, and `updating` will now be `true` as well. This allows the edge case of updating your UI to show that updates to the already loaded data are incoming.
137 |
138 | #### `respectCache`
139 |
140 | By default, when you directly `run()` an action, we ignore the cached values and initiate an entire new action run from the beginning. You can think of a `run()` as if we're running our action like we would a regular promise.
141 |
142 | But there are times when you do actually want to hit the cache on a direct run, specifically when you are making use of a [post-action hook](async-post-action-hook.md) - where you just want your run of the action to trigger the relevant UI updates that are associated with this action's result, for example.
143 |
144 | #### `ignoreShortCircuit`
145 |
146 | If set to `true`, will not run the [short circuit hook](async-short-circuit-hook.md) for this run of the action.
147 |
148 | ## `InjectAsyncAction` component
149 |
150 | You could also inject Async Action state directly into your React app without a hook.
151 |
152 | This is particularly useful for things like watching the state of an image loading. If we take this Async Action as an example:
153 |
154 | ```tsx
155 | async function loadImageFully(src: string) {
156 | return new Promise((resolve, reject) => {
157 | let img = new Image();
158 | img.onload = resolve;
159 | img.onerror = reject;
160 | img.src = src;
161 | });
162 | }
163 |
164 | export const AsyncActionImageLoad = createAsyncAction<{ src: string }>(async ({ src }) => {
165 | await loadImageFully(src);
166 | return successResult();
167 | });
168 | ```
169 |
170 | We can inject the async state of loading an image directly into our App using ``:
171 |
172 | ```tsx
173 |
177 | {([finished]) => {
178 | return
186 |
187 | }}
188 |
189 | ```
190 |
191 | We've very quickly made our App have images which will fade in once completely loaded! (You'd probably want to turn this into a component of its own and simply use the hooks - but as an example its fine for now)
192 |
193 | You can make use of the exported `EAsyncActionInjectType` which provides you with `BECKON` or `WATCH` constant variables - or you can provide them as a strings directly `"beckon"` or `"watch"`.
194 |
195 | ## Clear an Async Action's cache
196 |
197 | ```tsx
198 | searchPicturesForTag.clearCache({ tag });
199 | ```
200 |
201 | Clears all known state about this action (specific to the passed arguments).
202 |
203 | * Any action that is still busy resolving will have its results ignored.
204 |
205 | * Any watched actions ( `useWatch()` ) will return to their neutral state (i.e. `started = false`)
206 |
207 | * Any beckoned actions (`useBeckon()`) will have their actions re-instigated anew.
208 |
209 | ## Clear the Async Action cache for *all* argument combinations
210 |
211 | ```tsx
212 | searchPicturesForTag.clearAllCache();
213 | ```
214 |
215 | This is the same as `clearCache()`, except it will clear the cache for every single argument combination (the "fingerprints" we spoke of before) that this action has seen.
216 |
217 | ## Clear the Async Action cache for unwatched argument combinations
218 |
219 | ```tsx
220 | searchPicturesForTag.clearAllUnwatchedCache();
221 | ```
222 |
223 | This will check which argument combinations are not being "watched' in your React app anymore (i.e. usages of `useWatch()` , `useBeckon()` or ` `), and will clear the cache for those argument combinations. Pending actions for these arguments are not cleared.
224 |
225 | This is useful for simple garbage collection in Apps which tend to show lots of ever-changing data - which most likely won't be returned to (perhaps data based on the current time).
226 |
227 | ## Get, Set and Update Async Action cache
228 |
229 | Pullstate provides three extra methods which allow you to introspect and even change the current value stored in the cache. they are as follows:
230 |
231 | ```tsx
232 | searchPicturesForTag.getCached(args, options);
233 | searchPicturesForTag.setCached(args, result, options);
234 | searchPicturesForTag.updateCached(args, updater, options);
235 | ```
236 |
237 | ### `getCached(args, options)`
238 |
239 | You pass the action arguments for which you expect a cached result from this action as the first parameter, and optionally you can pass the following `options`:
240 |
241 | ```tsx
242 | {
243 | checkCacheBreak: boolean; // default = false
244 | }
245 | ```
246 |
247 | If `true` is passed here, then our [`cacheBreakHook`](async-cache-break-hook.md) for this action will be checked, and if this cache can be broken at the moment - `cacheBreakable` will be set to `true` in the response.
248 |
249 | The function will return an object which represents the current state of our cache for the passed arguments:
250 |
251 | ```tsx
252 | {
253 | started: boolean;
254 | finished: boolean;
255 | result: {
256 | error: boolean;
257 | payload: any;
258 | message: string;
259 | tags: string[];
260 | };
261 | updating: boolean;
262 | existed: boolean;
263 | cacheBreakable: boolean;
264 | timeCached: number;
265 | }
266 | ```
267 |
268 | If no cached value is found `existed` will be `false`.
269 |
270 | ### `setCached(args, result, options)`
271 |
272 | You pass the arguments you'd like to set the cached value for as the first parameter, and the new cached `result` value as the second parameter:
273 |
274 | ```tsx
275 | {
276 | error: boolean;
277 | payload: any;
278 | message: string;
279 | tags: string[];
280 | }
281 | ```
282 |
283 | (Hint: You can use convenience functions [`successResult()`](async-actions-creating.md#convenience-function-for-success) and [`errorResult()`](async-actions-creating.md#convenience-function-for-error) to help with this )
284 |
285 | A convenience method also exists for the majority of circumstances when you are just setting a success payload:
286 |
287 | ```tsx
288 | setCachedPayload(args, payload, options)
289 | ```
290 |
291 | You can provide an `options` object to either of these methods:
292 |
293 | ```tsx
294 | {
295 | notify?: boolean; // default = true
296 | }
297 | ```
298 |
299 | If `notify` is `true` (the default), then any listeners on this Async Action for these arguments will be notified and reflect the changes of the new cached value.
300 |
301 | It has no return value.
302 |
303 | ### `updateCached(args, updater, options)`
304 |
305 | This is similar to `setCached()`, but only runs on an already cached and non-error state cached value. Hence, we only need to affect `payload` on the result object.
306 |
307 | It works exactly the same as a regular store `update()`, except it acts on the currently cached `payload` value for the passed arguments. So, `updater` is a function that looks like this:
308 |
309 | ```tsx
310 | (currentlyCachedPayload) => {
311 | // directly mutate currentlyCachedPayload here
312 | };
313 | ```
314 |
315 | Optionally you can provide some options:
316 |
317 | ```tsx
318 | notify?: boolean; // default = true
319 | resetTimeCached?: boolean; // default = true
320 | ```
321 |
322 | `notify` is the same as in `setCached()`.
323 |
324 | If `resetTimeCached` is `true` Pullstate will internally set a new value for `timeCached` to the current time.
325 |
--------------------------------------------------------------------------------
/docs/async-actions-creating.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: async-actions-creating
3 | title: Creating an Async Action
4 | sidebar_label: Creating an Async Action
5 | ---
6 |
7 | **Note the tabs in these examples. If you are server-rendering, switch to the "Server-rendered app" tab.**
8 |
9 | Create an Async Action like so:
10 |
11 |
12 |
13 |
14 | ```tsx
15 | import { createAsyncAction } from "pullstate";
16 |
17 | const myAsyncAction = createAsyncAction(action, hooksAndOptions);
18 | ```
19 |
20 |
21 |
22 | ```tsx
23 | import { PullstateCore } from "./PullstateCore";
24 |
25 | const myAsyncAction = PullstateCore.createAsyncAction(action, hooksAndOptions);
26 | ```
27 |
28 | Server-rendered apps need to make use of your "core" Pullstate object to create Async Actions which can pre-fetch on the server.
29 |
30 | > Some of these examples will be making use of **client-side** only code to keep things simple and rather focus on the differences between TypeScript and JavaScript interactions. The server-rendering considerations to convert such code is explained in other examples, in the relevant tabs.
31 |
32 |
33 |
34 | We pass in two arguments. First, our actual `action`, and secondly, any [`hooks`](async-hooks-overview.md) we would like to set on this action to extend its functionality.
35 |
36 | ## The action itself
37 |
38 | The argument we pass in for `action` is pretty much just a standard `async` / `Promise`-returning function, but there are some extra considerations we need to keep in mind.
39 |
40 | To illustrate these considerations, lets use an example Async Action (fetching pictures related to a tag from an API) and its usage:
41 |
42 |
43 |
44 |
45 | ```tsx
46 | import { createAsyncAction, errorResult, successResult } from "pullstate";
47 |
48 | const searchPicturesForTag = createAsyncAction(async ({ tag }) => {
49 | const result = await PictureApi.searchWithTag(tag);
50 |
51 | if (result.success) {
52 | return successResult(result.pictures);
53 | }
54 |
55 | return errorResult([], `Couldn't get pictures: ${result.errorMessage}`);
56 | });
57 |
58 | export const PictureExample = props => {
59 | const [finished, result] = searchPicturesForTag.useBeckon({ tag: props.tag });
60 |
61 | if (!finished) {
62 | return Loading Pictures for tag "{props.tag}"
;
63 | }
64 |
65 | if (result.error) {
66 | return {result.message}
;
67 | }
68 |
69 | return ;
70 | };
71 | ```
72 |
73 |
74 |
75 | ```tsx
76 | import { createAsyncAction, errorResult, successResult } from "pullstate";
77 |
78 | interface IOSearchPicturesForTagInput {
79 | tag: string;
80 | }
81 |
82 | interface IOSearchPicturesForTagOutput {
83 | pictures: Picture[];
84 | }
85 |
86 | const searchPicturesForTag = createAsyncAction(
87 | async ({ tag }) => {
88 | const result = await PictureApi.searchWithTag(tag);
89 |
90 | if (result.success) {
91 | return successResult({ pictures: result.pictures });
92 | }
93 |
94 | return errorResult([], `Couldn't get pictures: ${result.errorMessage}`);
95 | }
96 | );
97 |
98 | export const PictureExample = (props: { tag: string }) => {
99 | const [finished, result] = searchPicturesForTag.useBeckon({ tag: props.tag });
100 |
101 | if (!finished) {
102 | return Loading Pictures for tag "{props.tag}"
;
103 | }
104 |
105 | if (result.error) {
106 | return {result.message}
;
107 | }
108 |
109 | return ;
110 | };
111 | ```
112 |
113 |
114 |
115 | ### The cachable "fingerprint"
116 |
117 | The first important concept to understand has to do with caching. For the **same arguments**, we do not want to be running these actions over and over again each time we hit them in our component code - what we really only want is the final result of these actions. So we need to be able to cache the results and re-use them where possible. Don't worry, Pullstate provides easy ways to ["break" this cache](async-cache-clearing.md) where needed as well.
118 |
119 | Pullstate does this by internally creating a "fingerprint" from the arguments which are passed in to the action. In our example here, the fingerprint is created from:
120 |
121 | ```tsx
122 | { tag: props.tag; }
123 | ```
124 |
125 | So, in the example, if on initial render we pass`{ tag: "dog" }` as props to our component, it will run the action for the first time with that fingerprint. Then, if we pass something new like `{ tag: "tree" }`, the action will run for that tag for the first time too. Both of these results are now cached per their arguments. If we pass `{ tag: "dog" }` again, the action will not run again but instead return our previously cached result.
126 |
127 | **Importantly:** Always have your actions defined with as many arguments which identify that single action as possible! (But no more than that - be as specific as possible while being as brief as possible).
128 |
129 | That said, there very well _could_ be reasons to create async actions that have no arguments and there are [ways you can cache bust](async-cache-clearing.md) actions to cause them to run again with the same "fingerprint".
130 |
131 | ### What to return from an action
132 |
133 | Your action should return a result structured in a certain way. Pullstate provides convenience methods for this, depending on whether you want to return an error or a success - as can be seen in the example where we return `successResult()` or `errorResult()`.
134 |
135 | This result structure is as follows:
136 |
137 | ```tsx
138 | {
139 | error: boolean;
140 | message: string;
141 | tags: string[];
142 | payload: any;
143 | }
144 | ```
145 |
146 | ### Convenience function for success
147 |
148 | Will set `{ error: false }` on the result object e.g:
149 |
150 | ```tsx
151 | // successResult(payload = null, tags = [], message = "") <- default arguments
152 | return successResult({ pictures: result.pictures });
153 | ```
154 |
155 | ### Convenience function for error
156 |
157 | Will set `{ error: true }` on the result object e.g:
158 |
159 | ```tsx
160 | // errorResult(tags = [], message = "", errorPayload = undefined) <- default arguments
161 | return errorResult(["NO_USER_FOUND"], "No user found in database by that name", errorPayload);
162 | ```
163 |
164 | The `tags` property here is a way to easily react to more specific error states in your UI. The default error result, when you haven't caught the errors yourself, will return with a single tag: `["UNKNOWN_ERROR"]`. If you return an error with `errorResult()`, the tag `"RETURNED_ERROR"` will automatically be added to tags. You may optionally also pass a `errorPayload` as a third argument if you need to access additional error data from the result.
165 |
166 | ## Update our state stores with async actions
167 |
168 | In our example we didn't actually touch our Pullstate stores, and that's just fine - there are many times where we just need to listen to asynchronous state without updating our stores (waiting for `Image.onload()` for example).
169 |
170 | But the Pullstate Way™ is generally to maintain our state in our stores for better control over things.
171 |
172 | A naive way to do this might be like so:
173 |
174 | **This code, while functionally correct, will cause unexpected behaviour!**
175 |
176 |
177 |
178 |
179 | ```tsx
180 | import { createAsyncAction, errorResult, successResult } from "pullstate";
181 | import { GalleryStore } from "./stores/GalleryStore";
182 |
183 | const searchPicturesForTag = createAsyncAction(async ({ tag }) => {
184 | const result = await PictureApi.searchWithTag(tag);
185 |
186 | if (result.success) {
187 | GalleryStore.update(s => {
188 | s.pictures = result.pictures;
189 | });
190 | return successResult();
191 | }
192 |
193 | return errorResult([], `Couldn't get pictures: ${result.errorMessage}`);
194 | });
195 |
196 | export const PictureExample = (props: { tag: string }) => {
197 | const [finished, result] = searchPicturesForTag.useBeckon({ tag: props.tag });
198 |
199 | if (!finished) {
200 | return Loading Pictures for tag "{props.tag}"
;
201 | }
202 |
203 | if (result.error) {
204 | return {result.message}
;
205 | }
206 |
207 | // Inside the Gallery component we will pull our state
208 | // from our stores directly instead of passing it as a prop
209 | return ;
210 | };
211 | ```
212 |
213 |
214 |
215 | ```tsx
216 | import { PullstateCore } from "./PullstateCore";
217 |
218 | const searchPicturesForTag = PullstateCore.createAsyncAction(
219 | async ({ tag }, { GalleryStore }) => {
220 | const result = await PictureApi.searchWithTag(tag);
221 |
222 | if (result.success) {
223 | GalleryStore.update(s => {
224 | s.pictures = result.pictures;
225 | });
226 | return successResult();
227 | }
228 |
229 | return errorResult([], `Couldn't get pictures: ${result.errorMessage}`);
230 | }
231 | );
232 | ```
233 |
234 | Something to notice here quick is that for server-rendered apps, we must make use of the second argument in our defined action which is the collection of stores being used on this render / server request.
235 |
236 | ```tsx
237 | export const PictureExample = (props: { tag: string }) => {
238 | const [finished, result] = searchPicturesForTag.useBeckon({ tag: props.tag });
239 |
240 | if (!finished) {
241 | return Loading Pictures for tag "{props.tag}"
;
242 | }
243 |
244 | if (result.error) {
245 | return {result.message}
;
246 | }
247 |
248 | // Inside the Gallery component we will pull our state
249 | // from our stores directly instead of passing it as a prop
250 | return ;
251 | };
252 | ```
253 |
254 |
255 |
256 | So what exactly is the problem? At first glance it might not be very clear.
257 |
258 | **The problem:** Because our actions are cached, when we return to a previously run action (with the same "fingerprint" of arguments) the action will not be run again, and our store will not be updated.
259 |
260 | To find out how to work with these scenarios, check out [Async Hooks](async-hooks-overview.md) - and specifically for this scenario, we would make use of the [`postActionHook()`](async-post-action-hook.md).
261 |
--------------------------------------------------------------------------------
/docs/async-actions-introduction.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: async-actions-introduction
3 | title: Introduction to Async Actions
4 | sidebar_label: Introduction
5 | ---
6 |
7 | More often than not, our stores do not exist in purely synchronous states. We often need to perform actions asynchronously, such as pulling data from an API.
8 |
9 | * It would be nice to have an easy way to keep our view up to date with the state of these actions **without putting too much onus on our stores directly** which quickly floods them with variables such as `userLoading`, `updatingUserInfo`, `userLoadError` etc - which we then have to make sure we're handling for each unique situation - it just gets messy quickly.
10 |
11 | * Having our views naturally listen for and initiate asynchronous state gets rid of a lot of boilerplate which we would usually need to write in `componentDidMount()` or the `useEffect()` hook.
12 |
13 | * There are also times where we are **server-rendering** and we would like to resolve our app's asynchronous state before rendering to the user. And again, without having to run something manually (and deal with all the edge cases manually too) for example:
14 |
15 | ```jsx
16 | try {
17 | const posts = await PostApi.getPostListForTag(tag);
18 |
19 | instance.stores.PostStore.update(s => {
20 | s.posts = posts;
21 | });
22 | } catch (e) {
23 | instance.stores.PostStore.update(s => {
24 | s.posts = [];
25 | s.postError = e.message;
26 | });
27 | }
28 | ```
29 |
30 | As you can imagine, separating this out and running such code for every case of state that you want pre-fetched before rendering to the client will get very verbose very quickly.
31 |
32 | Pullstate provides a much easier way to handle async scenarios through **Async Actions**!
--------------------------------------------------------------------------------
/docs/async-actions-other-options.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: async-actions-other-options
3 | title: Other Async Action Options
4 | sidebar_label: Other Async Action Options
5 | ---
6 |
7 | There are some other options we can pass in as the second value when creating Async Actions.
8 |
9 | ```tsx
10 | import { createAsyncAction } from "pullstate";
11 |
12 | const myAsyncAction = createAsyncAction(action, hooksAndOptions);
13 | ```
14 |
15 | Besides hooks, we can also (optionally) pass in:
16 |
17 | ```
18 | {
19 | subsetKey: (args: any) => string;
20 | forceContext: boolean;
21 | }
22 | ```
23 |
24 | ## `subsetKey`
25 |
26 | This is a function you can pass in, which intercepts the creation of this Async Action's "fingerprint" (or _key_).
27 |
28 | Basically, it takes in the current arguments passed to the action and returns a fingerprint to use in the cache. This could potentially give a performance boost when you are passing in really large argument sets.
29 |
30 | ## `forceContext`
31 |
32 | You can pass in `true` here in order to force this action to use Pullstate's context to grab its caching and execution state. This is useful if you have defined your Async Action outside of your current project, and without `PullstateCore` (see ["Creating an Async Action"](async-actions-creating.md) - under "Server-rendered app"). It basically makes an action which might seem "client-only" in its creation, force itself to act like a SSR action, using whatever Pullstate context is available.
33 |
--------------------------------------------------------------------------------
/docs/async-cache-break-hook.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: async-cache-break-hook
3 | title: Cache break hook
4 | sidebar_label: Cache break hook
5 | ---
6 |
7 | The cache break hook has the following API:
8 |
9 | ```tsx
10 | cacheBreakHook({ args, result, stores, timeCached }) => true | false
11 | ```
12 |
13 | > As per all Async Action things, `stores` here is only available as an option if you are making use of `` in your app (server-side rendering).
14 |
15 | It should return `true` or `false`.
16 |
17 | This action will only run if a cached result is found for this action (i.e. this action has completed already in the past). If you return `true`, this will "break" the currently cached value for this action. This action will now run again.
18 |
19 | Be sure to check out the [async hooks flow diagram](async-hooks-overview.md#async-hooks-flow-diagram) to understand better where this hook fits in.
20 |
21 | ## Example of a cache break hook
22 |
23 | _Deciding to not used the cached result from a search API when the search results are more than 30 seconds old_
24 |
25 | ```tsx
26 | const THIRTY_SECONDS = 30 * 1000;
27 |
28 | // The cache break hook in your action creator
29 |
30 | cacheBreakHook: ({ result, timeCached }) =>
31 | !result.error && timeCached + THIRTY_SECONDS < Date.now(),
32 | ```
33 |
34 | In this example want to break the cached result if it is not an error, and the `timeCached` is older than 30 seconds from `Date.now()`. `timeCached` is passed in, and is the millisecond epoch time of when our action last completed.
35 |
36 | You can create customized caching techniques as you see fit. Here we simply check against `timeCached`. Potentially, you might want to check other variables set in your stores, something set on your response payload or even use one of the passed arguments to affect caching length.
37 |
38 | Be sure to check out [the section on cache clearing](async-cache-clearing.md) for other ways to deal with cache invalidation.
39 |
--------------------------------------------------------------------------------
/docs/async-cache-clearing.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: async-cache-clearing
3 | title: Async cache clearing
4 | sidebar_label: Cache clearing
5 | ---
6 |
7 | A big part of working with asynchronous data is being able to control the cache.
8 |
9 | There's even a famous quote for it:
10 |
11 | > _"There are only two hard things in Computer Science: cache invalidation and naming things."_
12 | >
13 | > -**Phil Karlton**
14 |
15 | To try and make at least one of those things a bit easier for you, Pullstate provides a few different ways of dealing with cache invalidation with your async actions.
16 |
17 | ## Direct cache invalidation
18 |
19 | There are three "direct" ways to invalidate the cache for an action:
20 |
21 | ### Clear the cache for specific arguments (fingerprint)
22 |
23 | [See more](async-action-use.md#clear-an-async-action-s-cache)
24 |
25 | ```tsx
26 | GetUserAction.clearCache({ userId });
27 | ```
28 |
29 | ### Clear the cache completely for an action (all combinations of arguments)
30 |
31 | [See more](async-action-use.md#clear-the-async-action-cache-for-all-argument-combinations)
32 |
33 | ```tsx
34 | GetUserAction.clearAllCache();
35 | ```
36 |
37 | ### Clear all unwatched cache for an action
38 |
39 | [See more](async-action-use.md#clear-the-async-action-cache-for-unwatched-argument-combinations)
40 |
41 | ```tsx
42 | GetUserAction.clearAllUnwatchedCache();
43 | ```
44 |
45 | ## Conditional cache invalidation
46 |
47 | There is also a way to check and clear the cache automatically, using something called a `cacheBreakHook` - which runs when an action is called which already has a cached result, and decides whether the current cached result is still worthy. Check out the [async hooks flow diagram](async-hooks-overview.md#async-hooks-flow-diagram) to better understand how this hook fits in.
48 |
49 | ### [Cache Break Hook](async-cache-break-hook.md)
50 |
51 | ```tsx
52 | cacheBreakHook: ({ result, args, timeCached }) => true | false
53 | ```
54 |
--------------------------------------------------------------------------------
/docs/async-hooks-overview.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: async-hooks-overview
3 | title: Async hooks overview
4 | sidebar_label: Hooks overview
5 | ---
6 |
7 | The second argument while creating Async Actions allows us to pass hooks for the action:
8 |
9 | ```tsx
10 | const searchPicturesForTag = createAsyncAction(async ({ tag }) => {
11 | // action code
12 | }, hooksGoHere);
13 | ```
14 |
15 | This `hooksGoHere` object has three hook types which we can set for this action, as follows:
16 |
17 | ```tsx
18 | {
19 | postActionHook,
20 | shortCircuitHook,
21 | cacheBreakHook
22 | }
23 | ```
24 |
25 | ## Async hooks flow diagram
26 |
27 | To try and give you a quick overview of how actions work with hooks, lets look at a top-down flow diagram of an Async Action's execution:
28 |
29 | 
30 |
31 | ## Quick overview of each
32 |
33 | ### `postActionHook({ args, result, stores, context })`
34 |
35 | Post action hook is for consistently running state updates after an action completes for the first time or hits a cached value.
36 |
37 | Read more on the [post action hook](async-post-action-hook.md).
38 |
39 | ### `shortCircuitHook({ args, stores })`
40 |
41 | The short circuit hook is for checking the current state of your app and manually deciding that an action does not actually need to be run, and returning a replacement resolved value yourself.
42 |
43 | Read more on the [short circuit hook](async-short-circuit-hook.md).
44 |
45 | ### `cacheBreakHook({ args, result, stores, timeCached })`
46 |
47 | This hook is run only when an action has already resolve at least once. It takes the currently cached value and decides whether we should "break" the cache and run the action again instead of returning it.
48 |
49 | Read more on the [cache break hook](async-cache-break-hook.md).
50 |
51 | ---
52 |
53 | **NOTE:** In all these hooks, `stores` is only available when you are doing server rendering and you have used your centralized Pullstate "Core" to create your Async Actions, and are making use of `` (see the server-rendering part [Creating an Async Action](async-actions-creating.md)). If you have a client-side only app, just import and use your stores directly.
54 |
--------------------------------------------------------------------------------
/docs/async-post-action-hook.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: async-post-action-hook
3 | title: Post action hook
4 | sidebar_label: Post action hook
5 | ---
6 |
7 | `postActionHook()` is a function which is run directly after your action has completed. **And most importantly**, this is also run after we hit an already resolved and cached value.
8 |
9 | This is useful for updating our app's state (mostly concerning views, organising action results into specific store state that's in the current app's focus) in a consistent manner after actions, whether we hit the cache or directly ran them for the first time.
10 |
11 | Be sure to check out the [async hooks flow diagram](async-hooks-overview.md#async-hooks-flow-diagram) to understand better where this hook fits in.
12 |
13 | ### Updating our regular state stores with Async Action data using `postActionHook`
14 |
15 | `postActionHook` is most often used to synchronise the asynchronous state returned after our Async Actions into our stores.
16 |
17 | Let's quickly look at our previously explored **naive** example from [Creating an Async Action](async-actions-creating.md):
18 |
19 | > If you are not server-rendering you can ignore `PullstateCore` here and create your actions directly with `createAsyncAction()` - you would then also be importing and using your stores directly as opposed to the stores parameter passed inside the action function
20 |
21 | **DO NOT DO THIS!**
22 |
23 | ```tsx
24 | const searchPicturesForTag = PullstateCore.createAsyncAction(async ({ tag }, stores) => {
25 | const result = await PictureApi.searchWithTag(tag);
26 |
27 | if (result.success) {
28 | stores.GalleryStore.update(s => {
29 | s.pictures = result.pictures;
30 | });
31 | return successResult();
32 | }
33 |
34 | return errorResult([], `Couldn't get pictures: ${result.errorMessage}`);
35 | });
36 | ```
37 |
38 | Here we are updating our `GalleryStore` inside the action. The problem with this is that upon hitting a cached value, this action will not be run again ([unless cache broken](async-cache-clearing.md)) - and hence the `pictures` state inside the store will not be replaced with the new pictures.
39 |
40 | In comes `postActionHook` to save the day:
41 |
42 | ```tsx
43 | const searchPicturesForTag = PullstateCore.createAsyncAction(
44 | async ({ tag }) => {
45 | const result = await PictureApi.searchWithTag(tag);
46 |
47 | if (result.success) {
48 | return successResult(result);
49 | }
50 |
51 | return errorResult([], `Couldn't get pictures: ${result.errorMessage}`);
52 | },
53 | {
54 | postActionHook: ({ result, stores }) => {
55 | if (!result.error) {
56 | stores.GalleryStore.update(s => {
57 | s.pictures = result.payload.pictures;
58 | });
59 | }
60 | },
61 | }
62 | );
63 | ```
64 |
65 | > _`stores` here is a server-rendering only argument. For client-side only rendering, import and update your stores directly_
66 |
67 | Notice how we removed the update logic from the action itself and moved it inside the post action hook. Now our state is guaranteed to be updated the same, no matter if we hit the cache or ran the action directly.
68 |
69 | ## API of `postActionHook`:
70 |
71 | ```tsx
72 | postActionHook(inputs) { // Do things with inputs for this async action run };
73 | ```
74 |
75 | `inputs` is the only argument passed to `postActionHook` and has a structure like so:
76 |
77 | ```tsx
78 | {
79 | args, result, stores, context;
80 | }
81 | ```
82 |
83 | > As per all Async Action things, `stores` here is only available as an option if you are making use of `` in your app (server-side rendering).
84 |
85 | - `args` are the arguments for this run of the Async Action
86 | - `result` is the result of the run
87 | - `stores` is an object with all your state stores (**server rendering only**)
88 | - `context` is the context of where this post action hook was run:
89 |
90 | `context` helps us know how we came to be running this post action hook, and allows us to block certain scenarios (and prevent duplicate runs in some scenarios). It all depends on your app and how you make use of async actions.
91 |
92 | `context` will be set to one of the following values:
93 |
94 | - `BECKON_HIT_CACHE`
95 | - Ran after a [`beckon`](async-action-use.md#beckon-an-async-action-react-hook) hit a cached value on this action (triggered after a UI change where old arguments put into `beckon()` again)
96 | - `WATCH_HIT_CACHE`
97 | - Ran after a [`watch`](async-action-use.md#watch-an-async-action-react-hook) hit a cached value on this action (triggered after a UI change where old arguments put into `watch()` again)
98 | - `RUN_HIT_CACHE`
99 | - Ran after we called [`run`](async-action-use.md#run-an-async-action-directly) on this action with `respectCache: true`, and the cache was hit on this action
100 | - `DIRECT_RUN`
101 | - Ran after we called [`run`](async-action-use.md#run-an-async-action-directly) on this action with `respectCache` not set to `true`
102 | - `SHORT_CIRCUIT`
103 | - Ran after the `shortCircuit` hook finished the action pre-maturely
104 | - `BECKON_RUN`
105 | - Ran after a [`beckon`](async-action-use.md#beckon-an-async-action-react-hook) instigated this action for the first time (or after cache break)
106 |
--------------------------------------------------------------------------------
/docs/async-server-rendering.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: async-server-rendering
3 | title: Resolving async state while server-rendering
4 | sidebar_label: Resolve async state on the server
5 | ---
6 |
7 | ## Universal fetching
8 |
9 | Any action that is making use of `useBeckon()` ([discussed in detail here](async-action-use.md)) in the current render tree can have its state resolved on the server before rendering to the client. This allows us to generate dynamic pages on the fly!
10 |
11 | As per our example in ["Creating an Async Action"](async-actions-creating.md)
12 |
13 | ```tsx
14 | // Create the action
15 | const searchPicturesForTag = PullstateCore.createAsyncAction(async ({ tag }) => {
16 | const result = await PictureApi.searchWithTag(tag);
17 |
18 | if (result.success) {
19 | return successResult(result.pictures);
20 | }
21 |
22 | return errorResult([], `Couldn't get pictures: ${result.errorMessage}`);
23 | });
24 |
25 | // Use the action state in our components
26 | export const PictureExample = (props: { tag: string }) => {
27 | const [finished, result] = searchPicturesForTag.useBeckon({ tag: props.tag });
28 |
29 | if (!finished) {
30 | return Loading Pictures for tag "{props.tag}"
;
31 | }
32 |
33 | if (result.error) {
34 | return {result.message}
;
35 | }
36 |
37 | // Inside the Gallery component we will pull our state
38 | // from our stores directly instead of passing it as a prop
39 | return ;
40 | };
41 | ```
42 |
43 | So looking directly at `beckon()`:
44 |
45 | ```tsx
46 | const [finished, result] = searchPicturesForTag.useBeckon({ tag: props.tag });
47 | ```
48 |
49 | Using server-side async resolving, when our app hydrates for the first time on the client - `finished` will be `true` and `result` will already be resolved.
50 |
51 | **But very importantly:** The asynchronous action code needs to be able to resolve on both the server and client - so make sure that your data-fetching functions are "isomorphic" or "universal" in nature. Examples of such functionality are the [Apollo Client](https://www.apollographql.com/docs/react/api/apollo-client.html) or [Wildcard API](https://github.com/brillout/wildcard-api).
52 |
53 | In our example here, this API code would have to be "universally" resolvable:
54 |
55 | ```tsx
56 | const result = await PictureApi.searchWithTag(tag);
57 | ```
58 |
59 | ## Excluding async state from resolving on the server
60 |
61 | If you wish to have the regular behaviour of `useBeckon()` (that it instigates the async action when hit) but you don't actually want the server to resolve this asynchronous state (you're happy for it to load on the client-side only). You can pass in an option to `useBeckon()`:
62 |
63 | ```tsx
64 | const [finished, result, updating] = GetUserAction.useBeckon({ userId }, { ssr: false });
65 | ```
66 |
67 | Passing in `ssr: false` will cause this action to be ignored in the server asynchronous state resolve cycle.
68 |
69 | ## Resolving async state on the server
70 |
71 | Pullstate provides two ways to resolve your async state on the server - re-rendering until all state is resolved, and resolving state by running the actions directly off your initiated Pullstate "Core" before rendering.
72 |
73 | ### Re-render until resolved
74 |
75 | Until there is a better way to crawl through your react tree, the current easiest way to resolve async state on the server-side while rendering your React app is to simply render it multiple times. This allows Pullstate to register which async actions are required to resolve before we do our final render for the client.
76 |
77 | Using the `instance` which we create from our `PullstateCore` object (see [Server Rendering Example](quick-example-server-rendered.md#gather-stores-under-a-core-collection)) of all our stores:
78 |
79 | ```tsx
80 | const instance = PullstateCore.instantiate({ ssr: true });
81 |
82 | // (1)
83 | const app = (
84 |
85 |
86 |
87 | )
88 |
89 | let reactHtml = ReactDOMServer.renderToString(app);
90 |
91 | // (2)
92 | while (instance.hasAsyncStateToResolve()) {
93 | await instance.resolveAsyncState();
94 | reactHtml = ReactDOMServer.renderToString(app);
95 | }
96 |
97 | // (3)
98 | const snapshot = instance.getPullstateSnapshot();
99 |
100 | const body = `
101 |
102 | ${reactHtml}`;
103 | ```
104 |
105 | As marked with numbers in the code:
106 |
107 | 1. Place your app into a variable for ease of use. After which, we do our initial rendering as usual - this will register the initial async actions which need to be resolved onto our Pullstate `instance`.
108 |
109 | 2. We enter into a `while()` loop using `instance.hasAsyncStateToResolve()`, which will return `true` unless there is no async state in our React tree to resolve. Inside this loop we immediately resolve all async state with `instance.resolveAsyncState()` before rendering again. This renders our React tree until all state is deeply resolved.
110 |
111 | 3. Once there is no more async state to resolve, we can pull out the snapshot of our Pullstate instance - and we stuff that into our HTML to be hydrated on the client.
112 |
113 | ### Resolve Async Actions outside of render
114 |
115 | If you really wish to avoid the re-rendering, Async Actions are runnable on your Pullstate `instance` directly as well. This will "pre-cache" these action responses and hence not require a re-render (`instance.hasAsyncStateToResolve()` will return false).
116 |
117 | We make use of the following API on our Pullstate instance:
118 |
119 | ```tsx
120 | await pullstateInstance.runAsyncAction(CreatedAsyncAction, args, options);
121 | ```
122 |
123 | The `options` parameter here is the same as that defined on the regular [`run()` method on an action](async-action-use.md#run-an-async-action-directly). The key options being, `ignoreShortCircuit` (default `false`) and `respectCache` (default `false`).
124 |
125 | If you are running actions on the client side again before rendering your app for the first time (perhaps using some kind of isomorphic routing library) - you should be passing the option `{ respectCache: true }` on the client so these actions do not run again.
126 |
127 | This example makes use of `koa` and `koa-router`, we inject our instance onto our request's `ctx.state` early on in the request so we can use it along the way until finally rendering our app.
128 |
129 | Put the Pullstate instance into the current context:
130 |
131 | ```tsx
132 | ServerReactRouter.get("/*", async (ctx, next) => {
133 | ctx.state.pullstateInstance = PullstateCore.instantiate({ ssr: true });
134 | await next();
135 | });
136 | ```
137 |
138 | Create the routes, which run the various required actions:
139 |
140 | ```tsx
141 | ServerReactRouter.get("/list-cron-jobs", async (ctx, next) => {
142 | await ctx.state.pullstateInstance.runAsyncAction(CronJobAsyncActions.getCronJobs, { limit: 30 });
143 | await next();
144 | });
145 |
146 | ServerReactRouter.get("/cron-job-detail/:cronJobId", async (ctx, next) => {
147 | const { cronJobId } = ctx.params;
148 | await ctx.state.pullstateInstance.runAsyncAction(CronJobAsyncActions.loadCronJob, { id: cronJobId });
149 | await next();
150 | });
151 |
152 | ServerReactRouter.get("/edit-cron-job/:cronJobId", async (ctx, next) => {
153 | const { cronJobId } = ctx.params;
154 | await ctx.state.pullstateInstance.runAsyncAction(CronJobAsyncActions.loadCronJob, { id: cronJobId });
155 | await next();
156 | });
157 | ```
158 |
159 | And render the app:
160 |
161 | ```tsx
162 | ServerReactRouter.get("*", async (ctx) => {
163 | const { pullstateInstance } = ctx.state;
164 |
165 | // render React app with pullstate instance
166 |
167 |
168 |
169 | ```
170 |
171 | > Even though you are resolving actions outside of the render cycle, **you still need to use** `` as its the only way to provide your pre-run action's state to your app during rendering. If you didn't put that instance into the provider, `useBeckon()` can't see the result and will have queued up another run the regular way (multiple renders required).
172 |
173 | The Async Actions you use on the server and the ones you use on the client are exactly the same - so they are really nice for server-rendered SPAs. Everything just runs and caches as needed.
174 |
175 | You could even pre-cache a few pages on the server at once if you like (depending on how big you want the initial page payload to be), and have instant page changes on the client (and Async Actions has custom [cache busting built in](async-cache-clearing.md) to invalidate async state which is too stale - such as the user taking too long to change the page).
--------------------------------------------------------------------------------
/docs/async-short-circuit-hook.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: async-short-circuit-hook
3 | title: Short circuit hook
4 | sidebar_label: Short circuit hook
5 | ---
6 |
7 | The short circuit hook has the following API:
8 |
9 | ```tsx
10 | shortCircuitHook({ args, stores }) => false | asyncActionResult
11 | ```
12 |
13 | > As per all Async Action things, `stores` here is only available as an option if you are making use of `` in your app (server-side rendering).
14 |
15 | It should either return `false` or an Async Action result.
16 |
17 | If you return an Async Action result, this will effectively "short-circuit" this action. The Promise for this action will not run, and the action will continue from the point directly after that: caching this result, running the [post-action hook](async-post-action-hook.md) and finishing.
18 |
19 | Be sure to check out the [async hooks flow diagram](async-hooks-overview.md#async-hooks-flow-diagram) to understand better where this hook fits in.
20 |
21 | ## Example of short circuit
22 |
23 | _Deciding not to run a search API when the current search term is less than 1 character - return an empty list straight away_
24 |
25 | ```tsx
26 | shortCircuitHook: ({ args }) => {
27 | if (args.text.length <= 1) {
28 | return successResult({ posts: [] });
29 | }
30 |
31 | return false;
32 | },
33 | ```
34 |
35 | In this example, if we have a term that's zero or one character's in length - we short-circuit a `successResult()`, an empty list, instead of wasting our connection on a likely useless query. If the text is 2 or more characters, we continue with the action.
--------------------------------------------------------------------------------
/docs/inject-store-state.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: inject-store-state
3 | title:
4 | sidebar_label:
5 | ---
6 |
7 | The entirety of the code for `` is as follows:
8 |
9 |
10 |
11 | ```jsx
12 | function InjectStoreState({ store, on, children }) {
13 | const state = useStoreState(store, on);
14 | return children(state);
15 | }
16 | ```
17 |
18 |
19 |
20 |
21 | `S` = Store State (entire store's state from which you select)
22 |
23 | `SS` = Sub State (which you are selecting to be returned in the child function):
24 |
25 | ```tsx
26 | interface IPropsInjectStoreState {
27 | store: Store;
28 | on?: (state: S) => SS;
29 | children: (output: SS) => React.ReactElement;
30 | }
31 |
32 | function InjectStoreState({
33 | store,
34 | on = s => s as any,
35 | children,
36 | }: IPropsInjectStoreState): React.ReactElement {
37 | const state: SS = useStoreState(store, on);
38 | return children(state);
39 | }
40 | ```
41 |
42 |
43 |
44 | ## Props
45 |
46 | As you can see from that, the component `` takes 3 props:
47 |
48 | * `store` - the store from which we are selecting the sub-state
49 | * `on` (optional) - a function which selects the sub-state you want from the store's state
50 | * If non provided, selects the entire store's state (not recommended generally, smaller selections result in less re-rendering)
51 | * The required `children` you pass inside the component is simply a function
52 | * The function executes with a single argument, the sub-state which you have selected
53 |
54 | ## Example
55 |
56 | ```tsx
57 | const GreetUser = () => {
58 | return (
59 |
60 | s.userName}>
61 | {userName => Hi, {userName}! }
62 |
63 |
64 | )
65 | }
66 | ```
67 |
--------------------------------------------------------------------------------
/docs/installation.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: installation
3 | title: Installation
4 | sidebar_label: Installation
5 | ---
6 |
7 | Let's get the installation out of the way:
8 |
9 | ```powershell
10 | yarn add pullstate
11 | ```
12 |
--------------------------------------------------------------------------------
/docs/quick-example-server-rendered.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: quick-example-server-rendered
3 | title: Quick example (server rendering)
4 | sidebar_label: Quick example (server rendering)
5 | ---
6 |
9 |
10 | ## Create a state store
11 |
12 | Let's dive right in and define and export our first **state store**, by passing an initial state to `new Store()`:
13 |
14 |
15 |
16 | ```jsx
17 | import { Store } from "pullstate";
18 |
19 | export const UIStore = new Store({
20 | isDarkMode: true,
21 | });
22 | ```
23 |
24 |
25 | ```tsx
26 | import { Store } from "pullstate";
27 |
28 | interface IUIStore {
29 | isDarkMode: boolean;
30 | }
31 |
32 | export const UIStore = new Store({
33 | isDarkMode: true,
34 | });
35 | ```
36 |
37 |
38 |
39 | ## Gather stores under a core collection
40 |
41 | Server-rendering requires that we create a central place to reference all our stores, and we do this using `createPullstateCore()`:
42 |
43 | ```tsx
44 | import { UIStore } from "./stores/UIStore";
45 | import { createPullstateCore } from "pullstate";
46 |
47 | export const PullstateCore = createPullstateCore({
48 | UIStore
49 | });
50 | ```
51 |
52 | In this example we only have a single store, but a regular app should have at least a few.
53 |
54 | ## Read our store's state
55 |
56 | Then, in React, we can start using the state of that store using a simple hook `useState()` on the store.
57 |
58 | For server-rendering we also need to make use of `useStores()` on`PullstateCore`, which we defined above.
59 |
60 | > If we were creating a client-only app, we would simply import `UIStore` directly and use it, but for server-rendering we need to get `UIStore` by calling `useStores()`, which uses React's context to get our unique stores for this render / server request
61 |
62 | ```tsx
63 | import * as React from "react";
64 | import { PullstateCore } from "./PullstateCore";
65 |
66 | export const App = () => {
67 | const { UIStore } = PullstateCore.useStores();
68 | const isDarkMode = UIStore.useState(s => s.isDarkMode);
69 |
70 | return (
71 |
76 |
Hello Pullstate
77 |
78 | );
79 | };
80 | ```
81 |
82 | The second argument to `useState()` over here (`s => s.isDarkMode`), is a selection function that ensures we select only the state that we actually need for this component. This is a big performance booster, as we only listen for changes (and if changed, re-render the component) on the exact returned values - in this case, simply the value of `isDarkMode`.
83 |
84 | If you are not using TypeScript, or want to forgo nice types, you could also pull in your store's using `useStores()` imported directly from `pullstate`:
85 |
86 | ```tsx
87 | import { useStores } from "pullstate";
88 |
89 | // in app component
90 | const { UIStore } = useStores();
91 | const isDarkMode = UIStore.useState(s => s.isDarkMode);
92 | ```
93 |
94 | ---
95 |
96 | ## Add interaction (update state)
97 |
98 | Great, so we are able to pull our state from `UIStore` into our App. Now lets add some basic interaction with a ``:
99 |
100 | ```tsx
101 | const { UIStore } = PullstateCore.useStores();
102 | const isDarkMode = UIStore.useState(s => s.isDarkMode);
103 |
104 | return (
105 |
110 |
Hello Pullstate
111 |
113 | UIStore.update(s => {
114 | s.isDarkMode = !isDarkMode;
115 | })
116 | }>
117 | Toggle Dark Mode
118 |
119 |
120 | );
121 | ```
122 |
123 | Notice how we call `update()` on `UIStore`, inside which we directly mutate the store's state. This is all thanks to the power of `immer`, which you can check out [here](https://github.com/immerjs/immer).
124 |
125 | Another pattern, which helps to illustrate this further, would be to actually define the action of toggling dark mode to a function on its own:
126 |
127 |
128 |
129 | ```tsx
130 | function toggleMode(s) {
131 | s.isDarkMode = !s.isDarkMode;
132 | }
133 |
134 | // ...in our code
135 | UIStore.update(toggleMode)}>Toggle Dark Mode
136 | ```
137 |
138 |
139 | ```tsx
140 | function toggleMode(s: IUIStore) {
141 | s.isDarkMode = !s.isDarkMode;
142 | }
143 |
144 | // ...in our code
145 | UIStore.update(toggleMode)}>Toggle Dark Mode
146 | ```
147 |
148 |
149 |
150 | Basically, to update our app's state all we need to do is create a function (inline arrow function or regular) which takes the current store's state and mutates it to whatever we'd like the next state to be.
151 |
152 | ## Server-rendering our app
153 |
154 | When server rendering we need to wrap our app with `` which is a context provider that passes down fresh stores to be used on each new client request. We get these fresh stores from our `PullstateCore` above, by calling `instantiate({ ssr: true })` on it:
155 |
156 | ```tsx
157 | import { PullstateCore } from "./state/PullstateCore";
158 | import ReactDOMServer from "react-dom/server";
159 | import { PullstateProvider } from "pullstate";
160 |
161 | // A server request
162 | async function someRequest(req) {
163 | const instance = PullstateCore.instantiate({ ssr: true });
164 |
165 | const preferences = await UserApi.getUserPreferences(id);
166 |
167 | instance.stores.UIStore.update(s => {
168 | s.isDarkMode = preferences.isDarkMode;
169 | });
170 |
171 | const reactHtml = ReactDOMServer.renderToString(
172 |
173 |
174 |
175 | );
176 |
177 | const body = `
178 |
179 | ${reactHtml}`;
180 |
181 | // do something with the generated html and send response
182 | }
183 | ```
184 |
185 | * Manipulate your state directly during your server's request by using the `stores` property of the instantiated object
186 |
187 | * Notice that we pass our Pullstate core instance into `` as `instance`
188 |
189 | * Lastly, we need to return this state to the client somehow. We call `getPullstateSnapshot()` on the instance, stringify it, escape a couple characters, and set it on `window.__PULLSTATE__`, to be parsed and hydrated on the client.
190 |
191 | ### Quick note
192 |
193 | This kind of code (pulling asynchronous state into your stores on the server and client):
194 |
195 | ```tsx
196 | const preferences = await UserApi.getUserPreferences(id);
197 |
198 | instance.stores.UIStore.update(s => {
199 | s.isDarkMode = preferences.isDarkMode;
200 | });
201 | ```
202 |
203 | Can be conceptually made much easier using Pullstate's [Async Actions](async-actions-introduction.md)!
204 |
205 | ## Client-side state hydration
206 |
207 | ```tsx
208 | const hydrateSnapshot = JSON.parse(window.__PULLSTATE__);
209 |
210 | const instance = PullstateCore.instantiate({ ssr: false, hydrateSnapshot });
211 |
212 | ReactDOM.render(
213 |
214 |
215 | ,
216 | document.getElementById("react-mount")
217 | );
218 | ```
219 |
220 | We create a new instance on the client using the same method as on the server, except this time we can pass the `hydrateSnapshot` and `ssr: false`, which will instantiate our new stores with the state where our server left off.
221 |
222 | ## Client-side only updates
223 |
224 | Something interesting to notice at this point, which can also apply with server-rendered apps, is that (for client-side only updates) we could just import `UIStore` directly and run `update()` on it:
225 |
226 | ```tsx
227 | import { UIStore } from "./UIStore";
228 |
229 | // ...in our code
230 | UIStore.update(toggleMode)}>Toggle Dark Mode
231 | ```
232 | And our components would be updated accordingly. We have freed our app's state from the confines of the component! This is one of the main advantages of Pullstate - allowing us to separate our state concerns from being locked in at the component level and manage things easily at a more global level from which our components listen and react (through our `useStoreState()` hooks).
233 |
234 | We still need to make use of the `PullstateCore.useStores()` hook and `` in order to pick up and render server-side updates and state, but once we have hydrated that state into our stores on the client side, we can interact with Pullstate stores just as we would if it were a client-only app - **but we must be sure that these actions are 100% client-side only**.
235 |
--------------------------------------------------------------------------------
/docs/quick-example.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: quick-example
3 | title: Quick example
4 | sidebar_label: Quick example
5 | ---
6 |
9 |
10 | ## Create a state store
11 |
12 | Let's dive right in and define and export our first **state store**, by passing an initial state to `new Store()`:
13 |
14 |
15 |
16 | ```jsx
17 | import { Store } from "pullstate";
18 |
19 | export const UIStore = new Store({
20 | isDarkMode: true,
21 | });
22 | ```
23 |
24 |
25 | ```tsx
26 | import { Store } from "pullstate";
27 |
28 | interface IUIStore {
29 | isDarkMode: boolean;
30 | }
31 |
32 | export const UIStore = new Store({
33 | isDarkMode: true,
34 | });
35 | ```
36 |
37 |
38 |
39 | ## Read our store's state
40 |
41 | Then, in React, we can start using the state of that store using a simple hook `useState()` on the store iteself:
42 |
43 | ```tsx
44 | import * as React from "react";
45 | import { UIStore } from "./UIStore";
46 |
47 | export const App = () => {
48 | const isDarkMode = UIStore.useState(s => s.isDarkMode);
49 |
50 | return (
51 |
56 |
Hello Pullstate
57 |
58 | );
59 | };
60 | ```
61 |
62 | The argument to `useState()` over here (`s => s.isDarkMode`), is a selection function that ensures we select only the state that we actually need for this component. This is a big performance booster, as we only listen for changes (and if changed, re-render the component) on the exact returned values - in this case, simply the value of `isDarkMode`.
63 |
64 | ---
65 |
66 | ## Add interaction (update state)
67 |
68 | Great, so we are able to pull our state from `UIStore` into our App. Now lets add some basic interaction with a ``:
69 |
70 | ```tsx
71 | return (
72 |
77 |
Hello Pullstate
78 |
80 | UIStore.update(s => {
81 | s.isDarkMode = !isDarkMode;
82 | })
83 | }>
84 | Toggle Dark Mode
85 |
86 |
87 | );
88 | ```
89 |
90 | Notice how we call `update()` on `UIStore`, inside which we directly mutate the store's state. This is all thanks to the power of `immer`, which you can check out [here](https://github.com/immerjs/immer).
91 |
92 | Another pattern, which helps to illustrate this further, would be to actually define the action of toggling dark mode to a function on its own:
93 |
94 |
95 |
96 | ```tsx
97 | function toggleMode(s) {
98 | s.isDarkMode = !s.isDarkMode;
99 | }
100 |
101 | // ...in our code
102 | UIStore.update(toggleMode)}>Toggle Dark Mode
103 | ```
104 |
105 |
106 | ```tsx
107 | function toggleMode(s: IUIStore) {
108 | s.isDarkMode = !s.isDarkMode;
109 | }
110 |
111 | // ...in our code
112 | UIStore.update(toggleMode)}>Toggle Dark Mode
113 | ```
114 |
115 |
116 |
117 | Basically, to update our app's state all we need to do is create a function (inline arrow function or regular) which takes the current store's state and mutates it to whatever we'd like the next state to be.
118 |
119 | ## Omnipresent state updating
120 |
121 | Something interesting to notice at this point is that we are just importing `UIStore` directly and running `update()` on it:
122 |
123 | ```tsx
124 | import { UIStore } from "./UIStore";
125 |
126 | // ...in our code
127 | UIStore.update(toggleMode)}>Toggle Dark Mode
128 | ```
129 |
130 | And our components are being updated accordingly. We have freed our app's state from the confines of the component! This is one of the main advantages of Pullstate - allowing us to separate our state concerns from being locked in at the component level and manage things easily at a more global level from which our components listen and react (through our `useState()` hooks).
131 |
--------------------------------------------------------------------------------
/docs/reactions.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: reactions
3 | title: Reactions
4 | sidebar_label: Reactions
5 | ---
6 |
7 | A reaction is very similar to running an `update()`, except that it's an update that runs when a certain value has changed in your store.
8 |
9 | Their API is very similar to [Subscriptions](subscribe.md) in the way they watch values, the difference being that Reactions allow you to react and change your store's state at the same time in a "batched" way. Subscriptions only send you the new values. The reason these two are separated is for performance reasons - if you do not need to react and change your store's state on an update, rather use [subscriptions](subscribe.md).
10 |
11 | Reactions on a store are run directly after a call to `update()` on that store. They check their watched value in the store, and if changed, react with further state updates as you define. This is all batched in one go before notifying our React components to re-render as needed.
12 |
13 | ## Creating a reaction
14 |
15 | You register a reaction by calling `createReaction()` on your store:
16 |
17 |
18 |
19 | ```jsx
20 | StoreName.createReaction(watch, reaction);
21 | ```
22 |
23 |
24 | ```tsx
25 | type TReactionFunction = (watched: T, draft: S, original: S, lastWatched: T) => void;
26 |
27 | StoreName.createReaction(watch: (state: S) => T, reaction: TReactionFunction): () => void
28 | ```
29 |
30 |
31 |
32 | Similar to how we select sub-state with `useStoreState()` and ``, the first argument, `watch`, is function which returns a sub-selection of the store's state. This is the value we will be watching for changes:
33 |
34 | ```
35 | storeState => storeState.watchedValue;
36 | ```
37 |
38 | The watched value is checked every time the store is updated. If the value has changed, the `reaction` function is run. This is done in a performant way - reactions are run directly after updates before notifying your React components to update.
39 |
40 | The `reaction` function:
41 |
42 | ```jsx
43 | (watched, draft, original, lastWatched) => { //do things };
44 | ```
45 |
46 | The new watched value will be passed as the first argument,`watched`
47 |
48 | The next two arguments are the same as those used whe running `update()` on a store:
49 |
50 | * You can mutate your store directly, using `draft`
51 |
52 | * `original` is passed as a performance consideration. It is exactly the same as `draft` but without all the `immer` magic. It's a plain object of your state.
53 | * **Why?** Referencing values directly on your `draft` object can be a performance hit in certain situations because of the way that immer works internally (JavaScript proxies) - so if you need to _reference_ the current store state, you should use `original`. But if you want to _change_ it, you use `draft`. [Read more in immer's docs](https://immerjs.github.io/immer/performance#for-expensive-search-operations-read-from-the-original-state-not-the-draft).
54 |
55 | The last argument is the last watched value, passed as a convenience for if you ever need to refer to it.
56 |
57 | ## Example
58 |
59 | Listening to a [Cron Tab](https://en.wikipedia.org/wiki/Cron#Overview) string value, `crontab`, and calculating other values such as a human readable time and the previous and next dates of the cron run:
60 |
61 | ```tsx
62 | CronJobStore.createReaction(s => s.crontab, (crontab, draft) => {
63 | if (crontab !== null) {
64 | const resp = utils.getTimesAndTextFromCronTab({ crontab });
65 | if (resp.positive) {
66 | draft.currentCronJobTimesAndText = resp.payload;
67 | } else {
68 | draft.currentCronJobTimesAndText = {
69 | text: `Bad crontab`,
70 | times: {
71 | prevTime: null,
72 | nextTime: null,
73 | },
74 | };
75 | }
76 | } else {
77 | draft.currentCronJobTimesAndText = {
78 | times: {
79 | prevTime: null,
80 | nextTime: null,
81 | },
82 | text: `No crontab`,
83 | };
84 | }
85 | }
86 | );
87 | ```
88 |
89 | ## Unsubscribe from a reaction (client-side only)
90 |
91 | You may unsubscribe from a reaction on the client side of your app by simply running the function which is returned when you created the reaction.
92 |
--------------------------------------------------------------------------------
/docs/redux-dev-tools.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: redux-dev-tools
3 | title: Redux Devtools
4 | sidebar_label: Redux Devtools
5 | ---
6 |
7 | Pullstate includes a simple way to plug into Redux's devtools, which are already well established and extensive.
8 |
9 | Simply include the following somewhere after your Store definitions:
10 |
11 | ```ts
12 | import { registerInDevtools, Store } from "pullstate";
13 |
14 | // Store definition
15 | const ExampleStore = new Store({
16 | //...
17 | });
18 |
19 | // Register as many or as few Stores as you would like to monitor in the devtools
20 | registerInDevtools({
21 | ExampleStore,
22 | });
23 | ```
24 |
25 | After this, you should be able to open the Redux devtools tab and see each Store registered and showing changes.
26 |
--------------------------------------------------------------------------------
/docs/subscribe.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: subscribe
3 | title: Subscribe
4 | sidebar_label: Subscribe
5 | ---
6 |
7 | Subscriptions are client-side only listeners to changes in your store's state.
8 |
9 | They are very similar to [Reactions](reactions.md), the difference being they only listen for state changes and send you the new state. Reactions allow you to react and change your store's state at the same time in a "batched" way. **Subscriptions only send you the new values.** The reason these two are separated is for performance reasons - if you do not need to react and change your store's state on an update, rather use subscriptions.
10 |
11 | Some uses include integrating with third-party tools, for times when you want to align your app's state changes with a change in the tool as well.
12 |
13 | ## Subscribe API
14 |
15 | ```tsx
16 | myStore.subscribe(watch, listener) => unsubscribe
17 | ```
18 |
19 | **First** argument `watch` - a function which selects the state you'd like to watch for changes:
20 |
21 | ```
22 | storeState => storeState.valueToWatch;
23 | ```
24 |
25 | **Second** argument `listener` - a callback function which is run when the watched value changes. The new watched value will be the first argument, and the entire store's state is the second. The third argument is the last watched value, passed as a convenience for if you ever need to refer to it.
26 |
27 | ```jsx
28 | (watched, allState, prevWatched) => { //do things };
29 | ```
30 |
31 | **Return** value `unsubscribe` is simply a function you can run in order to stop listening.
32 |
33 | ## Subscribe Examples
34 |
35 | ### Leaflet tile change
36 |
37 | _(Listening for an option change in our store to update a Leaflet tile layer's source)_
38 |
39 | ```tsx
40 | // a useEffect() hook in a functional component
41 |
42 | useEffect(() => {
43 | const tileLayer = L.tileLayer(tileTemplate.url, {
44 | minZoom: 3,
45 | maxZoom: 18,
46 | }).addTo(mapRef.current);
47 |
48 | const unsubscribeFromTileTemplate = GISStore.subscribe(
49 | s => s.tileLayerTemplate,
50 | newTemplate => {
51 | tileLayer.setUrl(newTemplate.url);
52 | }
53 | );
54 |
55 | return () => {
56 | unsubscribeFromTileTemplate();
57 | };
58 | }, []);
59 | ```
60 |
61 | As you can see from the example, we listen to the `tileLayerTemplate` value in `GISStore`. If that changes, we want to set our Leaflet `tileLayer` to the new url, which will change the source of images used for our map's tiles.
62 |
63 | Also note that the value returned from `subscribe()` is a function. We should call this function to remove the listener when we don't need it anymore - here in the "cleanup" returned function of `useEffect()`.
64 |
65 | ---
66 |
67 | ### Firebase Realtime Database
68 |
69 | Another interesting use-case could be subscribing to a Firebase realtime data node, based on a certain value in your store.
70 |
71 | ```tsx
72 | let previousCityUnsubscribe = () => null;
73 |
74 | function startWatchingCity(cityCode) {
75 | return db.collection("cities")
76 | .doc(cityCode)
77 | .onSnapshot(function(doc) {
78 | CityStore.update(s => {
79 | s.watchedCities[cityCode] = { updated: new Date(), data: doc.data() };
80 | });
81 | });
82 | }
83 |
84 | function getRealtimeUpdatesForCityCode(cityCode) {
85 | previousCityUnsubscribe();
86 | previousCityUnsubscribe = startWatchingCity(cityCode);
87 | }
88 |
89 | CityStore.subscribe(s => s.currentCityCode, getRealtimeUpdatesForCityCode);
90 |
91 | // inside some initialization code on the client (run after initial
92 | // store hydration, if any), start watching initial city
93 | function initializeClientThings() {
94 | getRealtimeUpdatesForCityCode(CityStore.getRawState().currentCityCode);
95 | }
96 | ```
97 |
98 | And now, any time you run an update somewhere in your app:
99 | ```tsx
100 | CityStore.update(s => {
101 | s.currentCityCode = newCode;
102 | })
103 | ```
104 |
105 | If the value has actually changed, the current realtime database listener should be unsubscribed and a new one created with the new `currentCityCode`.
--------------------------------------------------------------------------------
/docs/update-store.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: update-store
3 | title: update()
4 | sidebar_label: update()
5 | ---
6 |
7 | You can update your store's state by calling `update()` directly on your store with an updater function passed in:
8 |
9 | ```tsx
10 | MyStore.update(updater | updater[])
11 | ```
12 |
13 | The updater function is simply a function which takes the store's current state and allows you to mutate it directly to create the next state. This is thanks to the power of [immer](https://github.com/immerjs/immer).
14 |
15 | Notice here that you can also pass multiple updater functions to `update()`. This allows us to be more modular with our store updates, and combine different updates together in a single batch.
16 |
17 | ### patches callback
18 |
19 | ```tsx
20 | MyStore.update(updater | updater[], patches)
21 | ```
22 |
23 | An optional second argument to `update()` is a patch callback - this is a very useful API provided by `immer`, and since `update()` is pretty much just a wrapper around Immer's functionality, we provide a way for you to make use of it in your Pullstate updates too. Read more about patches in `immer` docs, [here](https://github.com/immerjs/immer#patches).
24 |
25 | Patches allow fun things such as undo / redo functionality and state time travel!
26 |
27 | ## Example for update()
28 |
29 | Add some basic interaction to your app with a ``:
30 |
31 | ```tsx
32 | return (
33 |
38 |
Hello Pullstate
39 |
41 | UIStore.update(s => {
42 | s.isDarkMode = !isDarkMode;
43 | })
44 | }>
45 | Toggle Dark Mode
46 |
47 |
48 | );
49 | ```
50 |
51 | Notice how we call `update()` on `UIStore` and pass the updater function in.
52 |
53 | Another pattern, which helps to illustrate this further, would be to actually define the action of toggling dark mode to a function on its own:
54 |
55 | ```tsx
56 | function toggleMode(s) {
57 | s.isDarkMode = !s.isDarkMode;
58 | }
59 |
60 | // ...in our code
61 | UIStore.update(toggleMode)}>Toggle Dark Mode
62 | ```
63 |
64 | Basically, to update our store's state all we need to do is create a function (inline arrow function or regular) which takes the current store's state and mutates it to whatever we'd like the next state to be.
65 |
66 | ## Example with Multiple Updaters
67 |
68 | As mentioned above, the `update()` method also allows you to pass in an array of multiple updaters. Allowing you to batch multiple, more modular, updates to your store:
69 |
70 | ```ts
71 | UIStore.update([setDarkMode, setTypography("Roboto)]);
72 | ```
73 |
--------------------------------------------------------------------------------
/docs/use-store-state-hook.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: use-store-state-hook
3 | title: useStoreState (hook)
4 | sidebar_label: useStoreState (hook)
5 | ---
6 |
7 | > **NB** This code can all be used in a slightly different (and perhaps more readable) way, with `YourStore.useState()`. It functions exactly the same as the methods below, except without the first `store` parameter (since we are already calling it on the store itself).
8 |
9 | The `useStoreState()` hook to be used in your functional components has the following API interface:
10 |
11 |
12 |
13 | ```jsx
14 | function useStoreState(store);
15 | function useStoreState(store, getSubState);
16 | function useStoreState(store, getSubState, dependencies);
17 | ```
18 |
19 |
20 | ```tsx
21 | function useStoreState(store: Store): S;
22 | function useStoreState(store: Store, getSubState: (state: S) => SS): SS;
23 | function useStoreState(store: Store, getSubState: (state: S) => SS, deps?: ReadonlyArray): SS;
24 | ```
25 |
26 | * `S` here is an interface that represents your entire store's state
27 | * `SS` is an interface which represents a selected sub-state from your store
28 |
29 |
30 |
31 | Let's go through each way of using it:
32 |
33 | ## Use a store's entire state
34 |
35 | Example:
36 |
37 | ```tsx
38 | const allUIState = useStoreState(UIStore);
39 |
40 | return (allUIState.isDarkMode ? : );
41 | ```
42 |
43 | * A Pullstate store is passed in as the first argument
44 | * We do not provide any `getSubState()` selection method as the second argument
45 | * The entire store's state is returned
46 | * This way of using `useStoreState()` is not recommended generally, as smaller selections result in less re-rendering
47 | * **Our component will be re-rendered every time _any value_ changes in `UIStore`**
48 |
49 | The above (excluding store argument) applies to this method too:
50 |
51 | ```tsx
52 | const allUIState = UIStore.useState();
53 | ```
54 |
55 | ## Use a sub-state of a store
56 |
57 | Example:
58 |
59 | ```tsx
60 | const isDarkMode = useStoreState(UIStore, s => s.isDarkMode);
61 |
62 | return (isDarkMode ? : );
63 | ```
64 |
65 | * The recommended way of using `useStoreState()`, as smaller selections result in less re-rendering
66 | * A Pullstate store is passed in as the first argument
67 | * We provide a `getSubState()` selection method as the second argument
68 | * Here selecting `isDarkMode` inside our UIStore's state
69 | * **Our component will be re-rendered only if the value of `isDarkMode` has changed in our store**
70 |
71 | The above (excluding store argument) applies to this method too:
72 |
73 | ```tsx
74 | const isDarkMode = UIStore.useState(s => s.isDarkMode);
75 | ```
76 |
77 | ### Return multiple values in sub-state
78 |
79 | ```tsx
80 | const { isDarkMode, isMobile } = useStoreState(UIStore, s => ({
81 | isDarkMode: s.isDarkMode,
82 | isMobile: s.isMobile
83 | }));
84 |
85 | // OR
86 |
87 | const { isDarkMode, isMobile } = UIStore.useState(s => ({
88 | isDarkMode: s.isDarkMode,
89 | isMobile: s.isMobile
90 | }));
91 | ```
92 |
93 | ## Use a sub-state of a store, dynamically
94 |
95 | If you ever need to grab the sub-state of the store, using some kind of dynamic value inside your `getSubState()` selector function - then you need to provide the 3rd option to `useStoreState()` - an array of dependencies.
96 |
97 | This acts pretty much exactly the same as the `useEffect()` React hook, in that it will reassess our selection if the dependency array ever changes.
98 |
99 | You can make use of it like so:
100 |
101 | ```tsx
102 | const MyComponent = ({ type }) => {
103 | const data = useStoreState(MyStore, (s) => s[type], [type]);
104 |
105 | // OR
106 |
107 | const data = MyStore.useState((s) => s[type], [type]);
108 |
109 | // do stuff with data
110 | }
111 | ```
112 |
113 | Now whenever the value of `type` changes, Pullstate will reassess our selection and we will pull the new value from our store correctly.
114 |
--------------------------------------------------------------------------------
/graphics/Icon on Light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lostpebble/pullstate/2a4651a8e16f543b484298592cf894c281c98c16/graphics/Icon on Light.png
--------------------------------------------------------------------------------
/graphics/Icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lostpebble/pullstate/2a4651a8e16f543b484298592cf894c281c98c16/graphics/Icon.png
--------------------------------------------------------------------------------
/graphics/logo-new.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lostpebble/pullstate/2a4651a8e16f543b484298592cf894c281c98c16/graphics/logo-new.png
--------------------------------------------------------------------------------
/graphics/logo-newest.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lostpebble/pullstate/2a4651a8e16f543b484298592cf894c281c98c16/graphics/logo-newest.png
--------------------------------------------------------------------------------
/graphics/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lostpebble/pullstate/2a4651a8e16f543b484298592cf894c281c98c16/graphics/logo.png
--------------------------------------------------------------------------------
/graphics/pullstate-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lostpebble/pullstate/2a4651a8e16f543b484298592cf894c281c98c16/graphics/pullstate-logo.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pullstate",
3 | "version": "1.25.0",
4 | "description": "Simple state stores using immer and React hooks",
5 | "main": "dist/index.js",
6 | "module": "dist/index.es.js",
7 | "esnext": "dist/index.es.js",
8 | "main:umd": "dist/pullstate.umd.js",
9 | "main:umd:min": "dist/pullstate.umd.min.js",
10 | "types": "dist/index.d.ts",
11 | "files": [
12 | "dist"
13 | ],
14 | "scripts": {
15 | "generate-typedoc": "typedoc src/index.ts",
16 | "test": "jest",
17 | "test-watch": "jest --watch",
18 | "clean": "rimraf ./dist",
19 | "build": "npm run clean && rollup -c --bundleConfigAsCjs",
20 | "uglify": "terser ./dist/index.js -o ./dist/index.js",
21 | "check-size": "minified-size ./dist/index.es.js",
22 | "check-size-cjs": "minified-size ./dist/index.js",
23 | "benchmark-all": "cross-env TS_NODE_PROJECT=./test/tsconfig.json node -r ts-node/register --max-old-space-size=4096 test/benchmark/all.ts",
24 | "benchmark-async-argument": "cross-env TS_NODE_PROJECT=./test/tsconfig.json node -r ts-node/register --max-old-space-size=4096 test/benchmark/benchmark-async-argument.ts",
25 | "benchmark-destructuring": "cross-env TS_NODE_PROJECT=./test/tsconfig.json node -r ts-node/register --max-old-space-size=4096 test/benchmark/benchmark-destructuring.ts",
26 | "benchmark-all-immer": "cross-env TS_NODE_PROJECT=./test/tsconfig.json node -r ts-node/register --max-old-space-size=4096 test/benchmark/benchmark-all-immer.ts",
27 | "benchmark-immer-without-stores": "cross-env TS_NODE_PROJECT=./test/tsconfig.json node -r ts-node/register --max-old-space-size=4096 test/benchmark/benchmark-immer-without-stores.ts",
28 | "benchmark-immer-with-stores": "cross-env TS_NODE_PROJECT=./test/tsconfig.json node -r ts-node/register --max-old-space-size=4096 test/benchmark/benchmark-immer-with-stores.ts"
29 | },
30 | "keywords": [
31 | "immer",
32 | "state",
33 | "store",
34 | "react",
35 | "hooks"
36 | ],
37 | "author": "Paul Myburgh",
38 | "license": "MIT",
39 | "dependencies": {
40 | "fast-deep-equal": "^3.1.3",
41 | "immer": "^9.0.16"
42 | },
43 | "repository": "https://github.com/lostpebble/pullstate",
44 | "devDependencies": {
45 | "@testing-library/jest-dom": "^5.16.5",
46 | "@testing-library/react": "^13.4.0",
47 | "@types/benchmark": "^2.1.2",
48 | "@types/jest": "29.2.3",
49 | "@types/lodash": "^4.14.190",
50 | "@types/react": "18.0.25",
51 | "@types/react-dom": "18.0.9",
52 | "benchmark": "^2.1.4",
53 | "cross-env": "^7.0.3",
54 | "in-publish": "^2.0.1",
55 | "jest": "29.3.1",
56 | "jest-dom": "^4.0.0",
57 | "jest-environment-jsdom": "29.3.1",
58 | "jest-environment-jsdom-global": "4.0.0",
59 | "js-beautify": "^1.14.7",
60 | "lodash": "^4.17.21",
61 | "minified-size": "^3.0.0",
62 | "prettier": "^2.8.0",
63 | "react-test-renderer": "^18.2.0",
64 | "@rollup/plugin-commonjs": "^23.0.3",
65 | "@rollup/plugin-typescript": "^10.0.0",
66 | "@rollup/plugin-node-resolve": "^15.0.1",
67 | "react": "^18.2.0",
68 | "react-dom": "^18.2.0",
69 | "rollup-plugin-typescript2": "^0.34.1",
70 | "rollup-plugin-terser": "^7.0.2",
71 | "rollup": "^3.5.0",
72 | "terser": "^3.16.1",
73 | "ts-jest": "^29.0.3",
74 | "ts-loader": "^9.4.1",
75 | "ts-node": "^10.9.1",
76 | "typedoc": "^0.23.21",
77 | "type-fest": "^3.3.0",
78 | "typescript": "4.9.3",
79 | "webpack": "^4.44.2",
80 | "webpack-cli": "^3.3.12"
81 | },
82 | "peerDependencies": {
83 | "react": "^16.12.0 || ^17.0.0 || ^18.0.0"
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import typescript from "rollup-plugin-typescript2";
2 | import { terser } from "rollup-plugin-terser";
3 | import { nodeResolve } from "@rollup/plugin-node-resolve";
4 | import commonjs from "@rollup/plugin-commonjs";
5 | import pkg from "./package.json" assert { type: "json" };
6 | // import typescript from "@rollup/plugin-typescript";
7 |
8 | export default [{
9 | input: "./src/index.ts",
10 | plugins: [
11 | typescript({
12 | typescript: require("typescript"),
13 | }),
14 | ],
15 | output: [
16 | {
17 | file: pkg.main,
18 | format: "cjs",
19 | compact: true,
20 | // dir: "dist"
21 | },
22 | {
23 | file: pkg.module,
24 | format: "es",
25 | compact: true,
26 | // dist: "dist"
27 | },
28 | ],
29 | external: [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})],
30 | }, {
31 | input: "./src/index.ts",
32 | plugins: [
33 | typescript({
34 | typescript: require("typescript"),
35 | }),
36 | nodeResolve(),
37 | commonjs(),
38 | ],
39 | output: [
40 | {
41 | file: pkg["main:umd"],
42 | format: "umd",
43 | name: "pullstate",
44 | globals: {
45 | "react": "React",
46 | "immer": "immer",
47 | },
48 | },
49 | {
50 | file: pkg["main:umd:min"],
51 | format: "umd",
52 | name: "pullstate",
53 | globals: {
54 | "react": "React",
55 | "immer": "immer",
56 | },
57 | plugins: [terser()],
58 | },
59 | ],
60 | external: [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})].filter(dep => dep !== "fast-deep-equal"),
61 | }];
62 |
--------------------------------------------------------------------------------
/src/InjectAsyncAction.ts:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | IAsyncActionBeckonOptions,
4 | IAsyncActionWatchOptions,
5 | IOCreateAsyncActionOutput,
6 | TPullstateAsyncBeckonResponse,
7 | TPullstateAsyncWatchResponse
8 | } from "./async-types";
9 | import { IPullstateAllStores } from "./PullstateCore";
10 |
11 | export enum EAsyncActionInjectType {
12 | WATCH = "watch",
13 | BECKON = "beckon",
14 | }
15 |
16 | interface IPropsInjectAsyncActionBase {
17 | action: IOCreateAsyncActionOutput ;
18 | args?: A;
19 | }
20 |
21 | export interface IPropsInjectAsyncActionBeckon
22 | extends IPropsInjectAsyncActionBase {
23 | type: EAsyncActionInjectType.BECKON;
24 | options?: IAsyncActionBeckonOptions ;
25 | children: (response: TPullstateAsyncBeckonResponse) => React.ReactElement;
26 | }
27 |
28 | export interface IPropsInjectAsyncActionWatch
29 | extends IPropsInjectAsyncActionBase {
30 | type: EAsyncActionInjectType.WATCH;
31 | children: (response: TPullstateAsyncWatchResponse) => React.ReactElement;
32 | options?: IAsyncActionWatchOptions;
33 | }
34 |
35 | export type TInjectAsyncActionProps = IPropsInjectAsyncActionBeckon | IPropsInjectAsyncActionWatch;
36 |
37 | export function InjectAsyncAction(
38 | props: TInjectAsyncActionProps
39 | ): React.ReactElement {
40 | if (props.type === EAsyncActionInjectType.BECKON) {
41 | const response = props.action.useBeckon(props.args, props.options);
42 | return props.children(response);
43 | }
44 |
45 | const response = props.action.useWatch(props.args, props.options);
46 | return props.children(response);
47 | }
48 |
--------------------------------------------------------------------------------
/src/InjectStoreState.ts:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Store } from "./Store";
3 | import { useStoreState } from "./useStoreState";
4 |
5 | export interface IPropsInjectStoreState {
6 | store: Store;
7 | on?: (state: S) => SS;
8 | children: (output: SS) => React.ReactElement;
9 | }
10 |
11 | export function InjectStoreState({
12 | store,
13 | on = s => s as any,
14 | children,
15 | }: IPropsInjectStoreState): React.ReactElement {
16 | const state: SS = useStoreState(store, on);
17 | return children(state);
18 | }
19 |
--------------------------------------------------------------------------------
/src/InjectStoreStateOpt.ts:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Store } from "./Store";
3 | import { useStoreStateOpt } from "./useStoreStateOpt";
4 | import { ObjectPath } from "./useStoreStateOpt-types";
5 | import type { GetWithPath } from "type-fest/get";
6 |
7 | export interface IPropsInjectStoreStateOpt<
8 | T extends readonly unknown[],
9 | S extends object = object,
10 | P extends ObjectPath = T extends ObjectPath ? T : never
11 | > {
12 | store: Store;
13 | paths: P;
14 | children: (output: GetWithPath) => React.ReactElement;
15 | }
16 |
17 | /*
18 | import { DeepTypeOfArray, TAllPathsParameter } from "./useStoreStateOpt-types";
19 |
20 | export interface IPropsInjectStoreStateOpt<
21 | S extends object = object,
22 | P extends TAllPathsParameter = TAllPathsParameter,
23 | O extends [
24 | DeepTypeOfArray,
25 | DeepTypeOfArray,
26 | DeepTypeOfArray,
27 | DeepTypeOfArray,
28 | DeepTypeOfArray,
29 | DeepTypeOfArray,
30 | DeepTypeOfArray,
31 | DeepTypeOfArray,
32 | DeepTypeOfArray,
33 | DeepTypeOfArray,
34 | DeepTypeOfArray
35 | ] = [
36 | DeepTypeOfArray,
37 | DeepTypeOfArray,
38 | DeepTypeOfArray,
39 | DeepTypeOfArray,
40 | DeepTypeOfArray,
41 | DeepTypeOfArray,
42 | DeepTypeOfArray,
43 | DeepTypeOfArray,
44 | DeepTypeOfArray,
45 | DeepTypeOfArray,
46 | DeepTypeOfArray
47 | ]
48 | > {
49 | store: Store;
50 | paths: P;
51 | children: (output: O) => React.ReactElement;
52 | }*/
53 |
54 | export function InjectStoreStateOpt<
55 | T extends readonly unknown[],
56 | S extends object = object,
57 | P extends ObjectPath = T extends ObjectPath ? T : never
58 | >({ store, paths, children }: IPropsInjectStoreStateOpt): React.ReactElement {
59 | const state = useStoreStateOpt(store, paths) as GetWithPath;
60 | return children(state);
61 | }
62 |
--------------------------------------------------------------------------------
/src/PullstateCore.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 | import { Store, TUpdateFunction } from "./Store";
3 | import { clientAsyncCache, createAsyncAction, createAsyncActionDirect } from "./async";
4 | import {
5 | IAsyncActionRunOptions,
6 | ICreateAsyncActionOptions,
7 | IOCreateAsyncActionOutput,
8 | IPullstateAsyncActionOrdState,
9 | IPullstateAsyncCache,
10 | IPullstateAsyncResultState,
11 | TPullstateAsyncAction,
12 | TPullstateAsyncRunResponse
13 | } from "./async-types";
14 |
15 | export interface IPullstateAllStores {
16 | [storeName: string]: Store;
17 | }
18 |
19 | export const PullstateContext = React.createContext | null>(null);
20 |
21 | export const PullstateProvider = (
22 | {
23 | instance,
24 | children
25 | }: {
26 | instance: PullstateInstance;
27 | children?: any;
28 | }) => {
29 | return {children} ;
30 | };
31 |
32 | let singleton: PullstateSingleton | null = null;
33 |
34 | export const clientStores: {
35 | internalClientStores: true;
36 | stores: IPullstateAllStores;
37 | loaded: boolean;
38 | } = {
39 | internalClientStores: true,
40 | loaded: false,
41 | stores: {}
42 | };
43 |
44 | export type TMultiStoreAction,
45 | S extends IPullstateAllStores = P extends PullstateSingleton ? ST : any> = (update: TMultiStoreUpdateMap) => void;
46 |
47 | interface IPullstateSingletonOptions {
48 | asyncActions?: {
49 | defaultCachingSeconds?: number;
50 | };
51 | }
52 |
53 | export class PullstateSingleton {
54 | // private readonly originStores: S = {} as S;
55 | // private updatedStoresInAct = new Set();
56 | // private actUpdateMap: TMultiStoreUpdateMap | undefined;
57 | options: IPullstateSingletonOptions = {};
58 |
59 | constructor(allStores: S, options: IPullstateSingletonOptions = {}) {
60 | if (singleton !== null) {
61 | console.error(
62 | `Pullstate: createPullstate() - Should not be creating the core Pullstate class more than once! In order to re-use pull state, you need to call instantiate() on your already created object.`
63 | );
64 | }
65 |
66 | singleton = this;
67 | // this.originStores = allStores;
68 | clientStores.stores = allStores;
69 | clientStores.loaded = true;
70 | this.options = options;
71 | }
72 |
73 | instantiate(
74 | {
75 | hydrateSnapshot,
76 | ssr = false,
77 | customContext
78 | }: { hydrateSnapshot?: IPullstateSnapshot; ssr?: boolean, customContext?: any } = {}): PullstateInstance {
79 | if (!ssr) {
80 | const instantiated = new PullstateInstance(clientStores.stores as S, false, customContext);
81 |
82 | if (hydrateSnapshot != null) {
83 | instantiated.hydrateFromSnapshot(hydrateSnapshot);
84 | }
85 |
86 | instantiated.instantiateReactions();
87 | return instantiated as PullstateInstance;
88 | }
89 |
90 | const newStores: IPullstateAllStores = {};
91 |
92 | for (const storeName of Object.keys(clientStores.stores)) {
93 | if (hydrateSnapshot == null) {
94 | newStores[storeName] = new Store(clientStores.stores[storeName]._getInitialState());
95 | } else if (hydrateSnapshot.hasOwnProperty(storeName)) {
96 | newStores[storeName] = new Store(hydrateSnapshot.allState[storeName]);
97 | } else {
98 | newStores[storeName] = new Store(clientStores.stores[storeName]._getInitialState());
99 | console.warn(
100 | `Pullstate (instantiate): store [${storeName}] didn't hydrate any state (data was non-existent on hydration object)`
101 | );
102 | }
103 |
104 | newStores[storeName]._setInternalOptions({
105 | ssr,
106 | reactionCreators: clientStores.stores[storeName]._getReactionCreators()
107 | });
108 | }
109 |
110 | return new PullstateInstance(newStores as S, true, customContext);
111 | }
112 |
113 | useStores(): S {
114 | // return useContext(PullstateContext)!.stores as S;
115 | return useStores();
116 | }
117 |
118 | useInstance(): PullstateInstance {
119 | return useInstance();
120 | }
121 |
122 | /*actionSetup(): {
123 | action: (update: TMultiStoreAction, S>) => TMultiStoreAction, S>;
124 | act: (action: TMultiStoreAction, S>) => void;
125 | // act: (action: (update: TMultiStoreUpdateMap) => void) => void;
126 | } {
127 | const actUpdateMap = {} as TMultiStoreUpdateMap;
128 | const updatedStores = new Set();
129 |
130 | for (const store of Object.keys(clientStores.stores)) {
131 | actUpdateMap[store as keyof S] = (updater) => {
132 | updatedStores.add(store);
133 | clientStores.stores[store].batch(updater);
134 | };
135 | }
136 |
137 | const action: (
138 | update: TMultiStoreAction, S>
139 | ) => TMultiStoreAction, S> = (action) => action;
140 | const act = (action: TMultiStoreAction, S>): void => {
141 | updatedStores.clear();
142 | action(actUpdateMap);
143 | for (const store of updatedStores) {
144 | clientStores.stores[store].flushBatch(true);
145 | }
146 | };
147 |
148 | return {
149 | action,
150 | act,
151 | };
152 | }*/
153 |
154 | createAsyncActionDirect(
155 | action: (args: A) => Promise,
156 | options: ICreateAsyncActionOptions = {}
157 | ): IOCreateAsyncActionOutput {
158 | return createAsyncActionDirect(action, options);
159 | // return createAsyncAction (async (args: A) => {
160 | // return successResult(await action(args));
161 | // }, options);
162 | }
163 |
164 | createAsyncAction (
165 | action: TPullstateAsyncAction ,
166 | // options: Omit, "clientStores"> = {}
167 | options: ICreateAsyncActionOptions = {}
168 | ): IOCreateAsyncActionOutput {
169 | // options.clientStores = this.originStores;
170 | if (this.options.asyncActions?.defaultCachingSeconds && !options.cacheBreakHook) {
171 | options.cacheBreakHook = (inputs) =>
172 | inputs.timeCached < Date.now() - this.options.asyncActions!.defaultCachingSeconds! * 1000;
173 | }
174 |
175 | return createAsyncAction (action, options);
176 | }
177 | }
178 |
179 | type TMultiStoreUpdateMap = {
180 | [K in keyof S]: (updater: TUpdateFunction ? T : any>) => void;
181 | };
182 |
183 | interface IPullstateSnapshot {
184 | allState: { [storeName: string]: any };
185 | asyncResults: IPullstateAsyncResultState;
186 | asyncActionOrd: IPullstateAsyncActionOrdState;
187 | }
188 |
189 | export interface IPullstateInstanceConsumable {
190 | stores: T;
191 |
192 | hasAsyncStateToResolve(): boolean;
193 |
194 | resolveAsyncState(): Promise;
195 |
196 | getPullstateSnapshot(): IPullstateSnapshot;
197 |
198 | hydrateFromSnapshot(snapshot: IPullstateSnapshot): void;
199 |
200 | runAsyncAction(
201 | asyncAction: IOCreateAsyncActionOutput ,
202 | args?: A,
203 | runOptions?: Pick, "ignoreShortCircuit" | "respectCache">
204 | ): TPullstateAsyncRunResponse;
205 | }
206 |
207 | class PullstateInstance
208 | implements IPullstateInstanceConsumable {
209 | private _ssr: boolean = false;
210 | private _customContext: any;
211 | private readonly _stores: T = {} as T;
212 | _asyncCache: IPullstateAsyncCache = {
213 | listeners: {},
214 | results: {},
215 | actions: {},
216 | actionOrd: {}
217 | };
218 |
219 | constructor(allStores: T, ssr: boolean, customContext: any) {
220 | this._stores = allStores;
221 | this._ssr = ssr;
222 | this._customContext = customContext;
223 | /*if (!ssr) {
224 | // console.log(`Instantiating Stores`, allStores);
225 | clientStores.stores = allStores;
226 | clientStores.loaded = true;
227 | }*/
228 | }
229 |
230 | private getAllUnresolvedAsyncActions(): Array> {
231 | return Object.keys(this._asyncCache.actions).map((key) => this._asyncCache.actions[key]());
232 | }
233 |
234 | instantiateReactions() {
235 | for (const storeName of Object.keys(this._stores)) {
236 | this._stores[storeName]._instantiateReactions();
237 | }
238 | }
239 |
240 | getPullstateSnapshot(): IPullstateSnapshot {
241 | const allState = {} as IPullstateSnapshot["allState"];
242 |
243 | for (const storeName of Object.keys(this._stores)) {
244 | allState[storeName] = this._stores[storeName].getRawState();
245 | }
246 |
247 | return { allState, asyncResults: this._asyncCache.results, asyncActionOrd: this._asyncCache.actionOrd };
248 | }
249 |
250 | async resolveAsyncState() {
251 | const promises = this.getAllUnresolvedAsyncActions();
252 | await Promise.all(promises);
253 | }
254 |
255 | hasAsyncStateToResolve(): boolean {
256 | return Object.keys(this._asyncCache.actions).length > 0;
257 | }
258 |
259 | get stores(): T {
260 | return this._stores;
261 | }
262 |
263 | get customContext(): any {
264 | return this._customContext;
265 | }
266 |
267 | async runAsyncAction(
268 | asyncAction: IOCreateAsyncActionOutput ,
269 | args: A = {} as A,
270 | runOptions: Pick, "ignoreShortCircuit" | "respectCache"> = {}
271 | ): TPullstateAsyncRunResponse {
272 | if (this._ssr) {
273 | (runOptions as IAsyncActionRunOptions)._asyncCache = this._asyncCache;
274 | (runOptions as IAsyncActionRunOptions )._stores = this._stores;
275 | (runOptions as IAsyncActionRunOptions )._customContext = this._customContext;
276 | }
277 |
278 | return await asyncAction.run(args, runOptions);
279 | }
280 |
281 | hydrateFromSnapshot(snapshot: IPullstateSnapshot) {
282 | for (const storeName of Object.keys(this._stores)) {
283 | if (snapshot.allState.hasOwnProperty(storeName)) {
284 | this._stores[storeName]._updateStateWithoutReaction(snapshot.allState[storeName]);
285 | } else {
286 | console.warn(`${storeName} didn't hydrate any state (data was non-existent on hydration object)`);
287 | }
288 | }
289 |
290 | clientAsyncCache.results = snapshot.asyncResults || {};
291 | clientAsyncCache.actionOrd = snapshot.asyncActionOrd || {};
292 | }
293 | }
294 |
295 | export function createPullstateCore(
296 | allStores: T = {} as T,
297 | options: IPullstateSingletonOptions = {}
298 | ) {
299 | return new PullstateSingleton(allStores, options);
300 | }
301 |
302 | export function useStores() {
303 | return useContext(PullstateContext)!.stores as T;
304 | }
305 |
306 | export function useInstance(): PullstateInstance {
307 | return useContext(PullstateContext)! as PullstateInstance;
308 | }
309 |
--------------------------------------------------------------------------------
/src/async-types.ts:
--------------------------------------------------------------------------------
1 | import { IPullstateAllStores } from "./PullstateCore";
2 | import { TUpdateFunction } from "./Store";
3 |
4 | type TPullstateAsyncUpdateListener = () => void;
5 |
6 | // [ started, finished, result, updating, timeCached ]
7 | export type TPullstateAsyncWatchResponse = [
8 | boolean,
9 | boolean,
10 | TAsyncActionResult,
11 | boolean,
12 | number
13 | ];
14 |
15 | // export type TPullstateAsync
16 |
17 | // [ started, finished, result, updating, postActionResult ]
18 | // export type TPullstateAsyncResponseCacheFull = [
19 | // boolean,
20 | // boolean,
21 | // TAsyncActionResult,
22 | // boolean,
23 | // TAsyncActionResult | true | null
24 | // ];
25 |
26 | // [finished, result, updating]
27 | export type TPullstateAsyncBeckonResponse = [
28 | boolean,
29 | TAsyncActionResult,
30 | boolean
31 | ];
32 | // [result]
33 | export type TPullstateAsyncRunResponse = Promise>;
34 |
35 | export interface IPullstateAsyncResultState {
36 | [key: string]: TPullstateAsyncWatchResponse;
37 | }
38 |
39 | export interface IPullstateAsyncActionOrdState {
40 | [key: string]: number;
41 | }
42 |
43 | export enum EAsyncEndTags {
44 | THREW_ERROR = "THREW_ERROR",
45 | RETURNED_ERROR = "RETURNED_ERROR",
46 | UNFINISHED = "UNFINISHED",
47 | DORMANT = "DORMANT",
48 | }
49 |
50 | interface IAsyncActionResultBase {
51 | message: string;
52 | tags: (EAsyncEndTags | T)[];
53 | }
54 |
55 | export interface IAsyncActionResultPositive extends IAsyncActionResultBase {
56 | error: false;
57 | payload: R;
58 | errorPayload: null;
59 | }
60 |
61 | export interface IAsyncActionResultNegative extends IAsyncActionResultBase {
62 | error: true;
63 | errorPayload: N;
64 | payload: null;
65 | }
66 |
67 | export type TAsyncActionResult =
68 | IAsyncActionResultPositive
69 | | IAsyncActionResultNegative;
70 |
71 | // Order of new hook functions:
72 |
73 | // shortCircuitHook = ({ args, stores }) => cachable response | false - happens only on uncached action
74 | // cacheBreakHook = ({ args, stores, result }) => true | false - happens only on cached action
75 | // postActionHook = ({ args, result, stores }) => void | new result - happens on all actions, after the async / short circuit has resolved
76 | // ----> postActionHook potentially needs a mechanism which allows it to run only once per new key change (another layer caching of some sorts expiring on key change)
77 |
78 | export type TPullstateAsyncShortCircuitHook = (inputs: {
79 | args: A;
80 | stores: S;
81 | }) => TAsyncActionResult | false;
82 |
83 | export type TPullstateAsyncCacheBreakHook = (inputs: {
84 | args: A;
85 | result: TAsyncActionResult;
86 | stores: S;
87 | timeCached: number;
88 | }) => boolean;
89 |
90 | export enum EPostActionContext {
91 | WATCH_HIT_CACHE = "WATCH_HIT_CACHE",
92 | BECKON_HIT_CACHE = "BECKON_HIT_CACHE",
93 | RUN_HIT_CACHE = "RUN_HIT_CACHE",
94 | READ_HIT_CACHE = "READ_HIT_CACHE",
95 | READ_RUN = "READ_RUN",
96 | SHORT_CIRCUIT = "SHORT_CIRCUIT",
97 | DIRECT_RUN = "DIRECT_RUN",
98 | BECKON_RUN = "BECKON_RUN",
99 | CACHE_UPDATE = "CACHE_UPDATE",
100 | }
101 |
102 | export type TPullstateAsyncPostActionHook = (inputs: {
103 | args: A;
104 | result: TAsyncActionResult;
105 | stores: S;
106 | context: EPostActionContext;
107 | }) => void;
108 |
109 | export interface IAsyncActionReadOptions {
110 | postActionEnabled?: boolean;
111 | cacheBreakEnabled?: boolean;
112 | key?: string;
113 | cacheBreak?: boolean | number | TPullstateAsyncCacheBreakHook
114 | }
115 |
116 | export interface IAsyncActionBeckonOptions extends IAsyncActionReadOptions {
117 | ssr?: boolean;
118 | holdPrevious?: boolean;
119 | dormant?: boolean;
120 | }
121 |
122 | export interface IAsyncActionWatchOptions extends IAsyncActionBeckonOptions {
123 | initiate?: boolean;
124 | }
125 |
126 | export interface IAsyncActionUseOptions extends IAsyncActionWatchOptions {
127 | onSuccess?: (result: R, args: A) => void;
128 | }
129 |
130 | export interface IAsyncActionUseDeferOptions extends Omit, "key"> {
131 | key?: string;
132 | holdPrevious?: boolean;
133 | onSuccess?: (result: R, args: A) => void;
134 | clearOnSuccess?: boolean;
135 | }
136 |
137 | export interface IAsyncActionRunOptions {
138 | treatAsUpdate?: boolean;
139 | ignoreShortCircuit?: boolean;
140 | respectCache?: boolean;
141 | key?: string;
142 | cacheBreak?: boolean | number | TPullstateAsyncCacheBreakHook
143 | _asyncCache?: IPullstateAsyncCache;
144 | _stores?: S;
145 | _customContext?: any;
146 | }
147 |
148 | export interface IAsyncActionGetCachedOptions {
149 | checkCacheBreak?: boolean;
150 | cacheBreak?: boolean | number | TPullstateAsyncCacheBreakHook ;
151 | key?: string;
152 | }
153 |
154 | export interface IGetCachedResponse {
155 | started: boolean;
156 | finished: boolean;
157 | result: TAsyncActionResult;
158 | updating: boolean;
159 | existed: boolean;
160 | cacheBreakable: boolean;
161 | timeCached: number;
162 | }
163 |
164 | export interface IAsyncClearCacheOptions {
165 | notify?: boolean;
166 | }
167 |
168 | export interface IAsyncActionSetOrClearCachedValueOptions extends IAsyncClearCacheOptions {
169 | key?: string;
170 | }
171 |
172 | export interface IAsyncActionUpdateCachedOptions extends IAsyncActionSetOrClearCachedValueOptions {
173 | resetTimeCached?: boolean;
174 | runPostActionHook?: boolean;
175 | }
176 |
177 | export type TAsyncActionUse = (
178 | args?: A,
179 | options?: IAsyncActionUseOptions
180 | ) => TUseResponse ;
181 |
182 | export type TAsyncActionUseDefer = (
183 | options?: IAsyncActionUseDeferOptions
184 | ) => TUseDeferResponse ;
185 |
186 | export type TAsyncActionBeckon = (
187 | args?: A,
188 | options?: IAsyncActionBeckonOptions
189 | ) => TPullstateAsyncBeckonResponse;
190 |
191 | export type TAsyncActionWatch = (
192 | args?: A,
193 | options?: IAsyncActionWatchOptions
194 | ) => TPullstateAsyncWatchResponse;
195 |
196 | export type TAsyncActionRun = (
197 | args?: A,
198 | options?: IAsyncActionRunOptions
199 | ) => TPullstateAsyncRunResponse;
200 |
201 | export type TAsyncActionClearCache = (args?: A, options?: IAsyncActionSetOrClearCachedValueOptions) => void;
202 |
203 | export type TAsyncActionClearAllCache = (options?: IAsyncClearCacheOptions) => void;
204 |
205 | export type TAsyncActionClearAllUnwatchedCache = (options?: IAsyncClearCacheOptions) => void;
206 |
207 | export type TAsyncActionGetCached = (
208 | args?: A,
209 | options?: IAsyncActionGetCachedOptions
210 | ) => IGetCachedResponse;
211 |
212 | export type TAsyncActionSetCached = (
213 | args: A,
214 | result: TAsyncActionResult,
215 | options?: IAsyncActionSetOrClearCachedValueOptions
216 | ) => void;
217 |
218 | export type TAsyncActionSetCachedPayload = (args: A, payload: R, options?: IAsyncActionSetOrClearCachedValueOptions) => void;
219 |
220 | export type TAsyncActionUpdateCached = (
221 | args: A,
222 | updater: TUpdateFunction,
223 | options?: IAsyncActionUpdateCachedOptions
224 | ) => void;
225 | export type TAsyncActionRead = (args?: A, options?: IAsyncActionReadOptions ) => R;
226 |
227 | export type TAsyncActionDelayedRun = (
228 | args: A,
229 | options: IAsyncActionRunOptions & { delay: number; clearOldRun?: boolean; immediateIfCached?: boolean }
230 | ) => () => void;
231 |
232 | export interface IOCreateAsyncActionOutput {
233 | use: TAsyncActionUse ;
234 | useDefer: TAsyncActionUseDefer ;
235 | read: TAsyncActionRead ;
236 | useBeckon: TAsyncActionBeckon ;
237 | useWatch: TAsyncActionWatch ;
238 | run: TAsyncActionRun ;
239 | delayedRun: TAsyncActionDelayedRun ;
240 | getCached: TAsyncActionGetCached ;
241 | setCached: TAsyncActionSetCached ;
242 | setCachedPayload: TAsyncActionSetCachedPayload ;
243 | updateCached: TAsyncActionUpdateCached ;
244 | clearCache: TAsyncActionClearCache ;
245 | clearAllCache: TAsyncActionClearAllCache;
246 | clearAllUnwatchedCache: TAsyncActionClearAllUnwatchedCache;
247 | }
248 |
249 | export interface IPullstateAsyncCache {
250 | results: IPullstateAsyncResultState;
251 | listeners: {
252 | [key: string]: {
253 | [watchId: string]: TPullstateAsyncUpdateListener;
254 | };
255 | };
256 | actions: {
257 | [key: string]: () => Promise>;
258 | };
259 | actionOrd: IPullstateAsyncActionOrdState;
260 | }
261 |
262 | export type TPullstateAsyncAction = (
263 | args: A,
264 | stores: S,
265 | customContext: any
266 | ) => Promise>;
267 |
268 | export interface ICreateAsyncActionOptions {
269 | forceContext?: boolean;
270 | // clientStores?: S;
271 | shortCircuitHook?: TPullstateAsyncShortCircuitHook ;
272 | cacheBreakHook?: TPullstateAsyncCacheBreakHook ;
273 | postActionHook?: TPullstateAsyncPostActionHook ;
274 | subsetKey?: (args: A) => any;
275 | actionId?: string | number;
276 | }
277 |
278 | // action.use() types
279 |
280 | export interface IUseDebouncedExecutionOptions {
281 | validInput?: (args: A) => boolean;
282 | equality?: ((argsPrev: A, argsNew: A) => boolean) | any;
283 | executeOptions?: Omit, "key" | "cacheBreak">;
284 | watchLastValid?: boolean;
285 | }
286 |
287 | export type TRunWithPayload = (func: (payload: R) => any) => any;
288 |
289 | export interface IBaseObjResponseUse {
290 | execute: (runOptions?: IAsyncActionRunOptions ) => TPullstateAsyncRunResponse;
291 | }
292 |
293 | export interface IBaseObjResponseUseDefer {
294 | execute: (args?: A, runOptions?: Omit, "key" | "cacheBreak">) => TPullstateAsyncRunResponse;
295 | hasCached: (args?: A, options?: { successOnly?: boolean } & Omit, "key">) => boolean;
296 | unwatchExecuted: () => void;
297 | useDebouncedExecution: (args: A, delay: number, options?: IUseDebouncedExecutionOptions) => void;
298 | args: A;
299 | key: string;
300 | }
301 |
302 | export interface IBaseObjResponse {
303 | isLoading: boolean;
304 | isFinished: boolean;
305 | isUpdating: boolean;
306 | isStarted: boolean;
307 | // isSuccess: boolean;
308 | // isFailure: boolean;
309 | clearCached: () => void;
310 | updateCached: (updater: TUpdateFunction, options?: IAsyncActionUpdateCachedOptions) => void;
311 | setCached: (result: TAsyncActionResult, options?: IAsyncActionSetOrClearCachedValueOptions) => void;
312 | setCachedPayload: (payload: R, options?: IAsyncActionSetOrClearCachedValueOptions) => void;
313 | endTags: (T | EAsyncEndTags)[];
314 | renderPayload: TRunWithPayload;
315 | message: string;
316 | raw: TPullstateAsyncWatchResponse;
317 | }
318 |
319 | export interface IBaseObjSuccessResponse extends IBaseObjResponse {
320 | payload: R;
321 | errorPayload: null;
322 | error: false;
323 | isSuccess: true;
324 | isFailure: false;
325 | }
326 |
327 | export interface IBaseObjErrorResponse extends IBaseObjResponse {
328 | payload: null;
329 | errorPayload: N;
330 | error: true;
331 | isFailure: true;
332 | isSuccess: false;
333 | }
334 |
335 | export type TUseResponse =
336 | (IBaseObjSuccessResponse
337 | | IBaseObjErrorResponse) & IBaseObjResponseUse;
338 |
339 | export type TUseDeferResponse =
340 | (IBaseObjSuccessResponse
341 | | IBaseObjErrorResponse) & IBaseObjResponseUseDefer;
342 |
--------------------------------------------------------------------------------
/src/batch.ts:
--------------------------------------------------------------------------------
1 | import { globalClientState } from "./globalClientState";
2 |
3 | interface IBatchState {
4 | uiBatchFunction: ((updates: () => void) => void);
5 | }
6 |
7 | const batchState: Partial = {};
8 |
9 | export function setupBatch({ uiBatchFunction }: IBatchState) {
10 | batchState.uiBatchFunction = uiBatchFunction;
11 | }
12 |
13 | export function batch(runUpdates: () => void) {
14 | if (globalClientState.batching) {
15 | throw new Error("Pullstate: Can't enact two batch() update functions at the same time-\n" +
16 | "make sure you are not running a batch() inside of a batch() by mistake.");
17 | }
18 |
19 | globalClientState.batching = true;
20 |
21 | try {
22 | runUpdates();
23 | } finally {
24 | if (batchState.uiBatchFunction) {
25 | batchState.uiBatchFunction(() => {
26 | Object.values(globalClientState.flushStores).forEach(store => store.flushBatch(true));
27 | });
28 | } else {
29 | Object.values(globalClientState.flushStores).forEach(store => store.flushBatch(true));
30 | }
31 | globalClientState.flushStores = {};
32 | globalClientState.batching = false;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/fastDeepEqual.d.ts:
--------------------------------------------------------------------------------
1 | declare module "fast-deep-equal/es6" {
2 | const equal: (a: any, b: any) => boolean;
3 | export = equal;
4 | }
5 |
--------------------------------------------------------------------------------
/src/globalClientState.ts:
--------------------------------------------------------------------------------
1 | import { Store } from "./Store";
2 |
3 | export const globalClientState: {
4 | storeOrdinal: number,
5 | batching: boolean;
6 | flushStores: {
7 | [storeName: number]: Store;
8 | };
9 | } = {
10 | storeOrdinal: 0,
11 | batching: false,
12 | flushStores: {}
13 | };
14 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { useStoreState } from "./useStoreState";
2 | import { Store, TStoreAction, TUpdateFunction, update } from "./Store";
3 | import { InjectStoreState } from "./InjectStoreState";
4 | import type { PullstateSingleton } from "./PullstateCore";
5 | import {
6 | createPullstateCore,
7 | IPullstateAllStores,
8 | IPullstateInstanceConsumable,
9 | PullstateContext,
10 | PullstateProvider,
11 | TMultiStoreAction,
12 | useInstance,
13 | useStores
14 | } from "./PullstateCore";
15 | import { createAsyncAction, createAsyncActionDirect, errorResult, successResult } from "./async";
16 | import { EAsyncActionInjectType, InjectAsyncAction, TInjectAsyncActionProps } from "./InjectAsyncAction";
17 | import { TUseResponse } from "./async-types";
18 | import { registerInDevtools } from "./reduxDevtools";
19 | import { useLocalStore } from "./useLocalStore";
20 | import { batch, setupBatch } from "./batch";
21 |
22 | export * from "./async-types";
23 |
24 | export {
25 | useStoreState,
26 | useLocalStore,
27 | update,
28 | Store,
29 | InjectStoreState,
30 | PullstateProvider,
31 | useStores,
32 | useInstance,
33 | createPullstateCore,
34 | createAsyncAction,
35 | createAsyncActionDirect,
36 | successResult,
37 | errorResult,
38 | // EAsyncEndTags,
39 | IPullstateInstanceConsumable,
40 | IPullstateAllStores,
41 | InjectAsyncAction,
42 | EAsyncActionInjectType,
43 | TInjectAsyncActionProps,
44 | // TPullstateAsyncAction,
45 | // TAsyncActionResult,
46 | TUpdateFunction,
47 | TStoreAction,
48 | TMultiStoreAction,
49 | PullstateContext,
50 | TUseResponse,
51 | registerInDevtools,
52 | batch,
53 | setupBatch
54 | };
55 |
56 | export type {
57 | PullstateSingleton
58 | };
59 |
--------------------------------------------------------------------------------
/src/reduxDevtools.ts:
--------------------------------------------------------------------------------
1 | import { IPullstateAllStores } from "./PullstateCore";
2 |
3 | interface IORegisterInDevtoolsOptions {
4 | namespace?: string;
5 | }
6 |
7 | export function registerInDevtools(stores: IPullstateAllStores, { namespace = "" }: IORegisterInDevtoolsOptions = {}) {
8 | const devToolsExtension = typeof window !== "undefined" ? (window as any)?.__REDUX_DEVTOOLS_EXTENSION__ : undefined;
9 |
10 | if (devToolsExtension) {
11 | for (const key of Object.keys(stores)) {
12 | const store = stores[key];
13 |
14 | const devTools = devToolsExtension.connect({ name: `${namespace}${key}` });
15 | devTools.init(store.getRawState());
16 | let ignoreNext = false;
17 | /*store.subscribe(
18 | (state) => {
19 | if (ignoreNext) {
20 | ignoreNext = false;
21 | return;
22 | }
23 | devTools.send("Change", state);
24 | },
25 | () => {}
26 | );*/
27 | store.subscribe(
28 | (s) => s,
29 | (watched) => {
30 | if (ignoreNext) {
31 | ignoreNext = false;
32 | return;
33 | }
34 | devTools.send("Change", watched);
35 | }
36 | );
37 |
38 | devTools.subscribe((message: { type: string; state: any }) => {
39 | if (message.type === "DISPATCH" && message.state) {
40 | ignoreNext = true;
41 | const parsed = JSON.parse(message.state);
42 | store.replace(parsed);
43 | }
44 | });
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/useLocalStore.ts:
--------------------------------------------------------------------------------
1 | import { Store } from "./Store";
2 | import { useRef } from "react";
3 | import isEqual from "fast-deep-equal/es6";
4 |
5 | function useLocalStore(initialState: (() => S) | S, deps?: ReadonlyArray): Store {
6 | const storeRef = useRef>();
7 |
8 | if (storeRef.current == null) {
9 | storeRef.current = new Store(initialState);
10 | }
11 |
12 | if (deps !== undefined) {
13 | const prevDeps = useRef>(deps);
14 | if (!isEqual(deps, prevDeps)) {
15 | storeRef.current = new Store(initialState);
16 | }
17 | }
18 |
19 | return storeRef.current;
20 | }
21 |
22 | export { useLocalStore };
23 |
--------------------------------------------------------------------------------
/src/useStoreState.ts:
--------------------------------------------------------------------------------
1 | import isEqual from "fast-deep-equal/es6";
2 | // S = State
3 | // SS = Sub-state
4 | import { useEffect, useRef, useState } from "react";
5 | import { Store } from "./Store";
6 |
7 | // const isEqual = require("fast-deep-equal/es6");
8 |
9 | export interface IUpdateRef {
10 | shouldUpdate: boolean;
11 | onStoreUpdate: (() => void) | null;
12 | getSubState?: (state: any) => any;
13 | currentSubState: any;
14 | setInitial: boolean;
15 | }
16 |
17 | export interface IUpdateRefNew {
18 | state: any;
19 | initialized: boolean;
20 | }
21 |
22 | const onServer = typeof window === "undefined";
23 |
24 | function useStoreState(store: Store): S;
25 | function useStoreState(
26 | store: Store,
27 | getSubState: (state: S) => SS,
28 | deps?: ReadonlyArray
29 | ): SS;
30 | function useStoreState(store: Store, getSubState?: (state: any) => any, deps?: ReadonlyArray): any {
31 | const updateRef = useRef({ state: undefined, initialized: false });
32 |
33 | if (!updateRef.current.initialized) {
34 | updateRef.current.state = getSubState ? getSubState(store.getRawState()) : store.getRawState();
35 | updateRef.current.initialized = true;
36 | }
37 |
38 | // useState with only a simple value to prevent double equality checks for the state
39 | const [, setUpdateTrigger] = useState(0);
40 |
41 | // const [current, setCurrent] = useState(() => {
42 | // return getSubState ? getSubState(store.getRawState()) : store.getRawState();
43 | // });
44 | // updateRef.current.state = current;
45 |
46 | useEffect(() => {
47 | const effectState = { shouldUpdate: true };
48 |
49 | function update() {
50 | if (effectState.shouldUpdate) {
51 | const nextSubState = getSubState
52 | ? getSubState(store.getRawState())
53 | : store.getRawState();
54 |
55 | if (!isEqual(updateRef.current.state, nextSubState)) {
56 | // final check again before actually running state update (might prevent no-op errors with React)
57 | if (effectState.shouldUpdate) {
58 | updateRef.current.state = nextSubState;
59 | setUpdateTrigger((val) => val + 1);
60 | }
61 | }
62 | }
63 | }
64 |
65 | store._addUpdateListener(update);
66 | // This ensures we get the latest updated value, even if it has changed
67 | // between the initial rendering and the effect
68 | update();
69 |
70 | return () => {
71 | effectState.shouldUpdate = false;
72 | store._removeUpdateListener(update);
73 | };
74 | }, deps ?? []);
75 |
76 | if (deps !== undefined) {
77 | const prevDeps = useRef>(deps);
78 | if (!isEqual(deps, prevDeps)) {
79 | updateRef.current.state = getSubState!(store.getRawState());
80 | }
81 | }
82 |
83 | return updateRef.current.state;
84 | }
85 |
86 | /*
87 | function useStoreState(store: Store, getSubState?: (state: any) => any, deps?: ReadonlyArray): any {
88 | const updateRef = useRef({
89 | shouldUpdate: true,
90 | onStoreUpdate: null,
91 | getSubState,
92 | currentSubState: null,
93 | setInitial: false,
94 | });
95 |
96 | const [, setUpdateTrigger] = useState(0);
97 |
98 | if (!updateRef.current.setInitial) {
99 | updateRef.current.currentSubState = updateRef.current.getSubState
100 | ? updateRef.current.getSubState(store.getRawState())
101 | : store.getRawState();
102 | updateRef.current.setInitial = true;
103 | }
104 |
105 | if (updateRef.current.onStoreUpdate === null) {
106 | updateRef.current.onStoreUpdate = function onStoreUpdate() {
107 | const nextSubState = updateRef.current.getSubState
108 | ? updateRef.current.getSubState(store.getRawState())
109 | : store.getRawState();
110 | if (updateRef.current.shouldUpdate && !isEqual(updateRef.current.currentSubState, nextSubState)) {
111 | // final check again before actually running state update (might prevent no-op errors with React)
112 | if (updateRef.current.shouldUpdate) {
113 | updateRef.current.currentSubState = nextSubState;
114 | setUpdateTrigger((val) => val + 1);
115 | }
116 | }
117 | };
118 |
119 | if (!onServer) {
120 | store._addUpdateListener(updateRef.current.onStoreUpdate!);
121 | }
122 | }
123 |
124 | useEffect(() => {
125 | updateRef.current.shouldUpdate = true;
126 |
127 | return () => {
128 | updateRef.current.shouldUpdate = false;
129 | store._removeUpdateListener(updateRef.current.onStoreUpdate!);
130 | };
131 | }, []);
132 |
133 | if (deps !== undefined) {
134 | const prevDeps = useRef>(deps);
135 | if (!isEqual(deps, prevDeps)) {
136 | updateRef.current.getSubState = getSubState;
137 | updateRef.current.currentSubState = getSubState!(store.getRawState());
138 | }
139 | }
140 |
141 | console.log(updateRef.current);
142 |
143 | return updateRef.current.currentSubState;
144 | }
145 | */
146 |
147 | export { useStoreState };
148 |
--------------------------------------------------------------------------------
/src/useStoreStateOpt-types.ts:
--------------------------------------------------------------------------------
1 | // prettier-ignore
2 |
3 | type ExtractObj = K extends keyof S ? S[K] : never
4 |
5 | export type ObjectPath =
6 | T extends readonly [infer T0, ...infer TR]
7 | ? TR extends []
8 | ? ExtractObj extends never
9 | ? readonly []
10 | : readonly [T0]
11 | : ExtractObj extends object
12 | ? readonly [T0, ...ObjectPath, TR>]
13 | : ExtractObj extends never
14 | ? readonly []
15 | : readonly [T0]
16 | : readonly []
17 |
18 | /*
19 | export interface DeepKeyOfArray extends Array {
20 | ["0"]: keyof O;
21 | ["1"]?: this extends {
22 | ["0"]: infer K0
23 | } ?
24 | K0 extends keyof O ?
25 | O[K0] extends Array ?
26 | number
27 | :
28 | keyof O[K0]
29 | :
30 | never
31 | :
32 | never;
33 | [rest: string]: any;
34 | }
35 |
36 | export type TAllPathsParameter =
37 | | [DeepKeyOfArray]
38 | | [DeepKeyOfArray, DeepKeyOfArray]
39 | | [DeepKeyOfArray, DeepKeyOfArray, DeepKeyOfArray]
40 | | [DeepKeyOfArray, DeepKeyOfArray, DeepKeyOfArray, DeepKeyOfArray]
41 | | [DeepKeyOfArray, DeepKeyOfArray, DeepKeyOfArray, DeepKeyOfArray, DeepKeyOfArray]
42 | | [DeepKeyOfArray, DeepKeyOfArray