From 54c206b92de7c32fa16b4ebfc32342e2855f832a Mon Sep 17 00:00:00 2001 From: Mohamed EL AYADI Date: Thu, 17 Nov 2022 20:56:11 +0100 Subject: [PATCH 1/6] hide instances created by standalone producer effects creator from devtools --- packages/react-async-states/src/async-state/AsyncState.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-async-states/src/async-state/AsyncState.ts b/packages/react-async-states/src/async-state/AsyncState.ts index f0a6e3b7..3d8ca378 100644 --- a/packages/react-async-states/src/async-state/AsyncState.ts +++ b/packages/react-async-states/src/async-state/AsyncState.ts @@ -919,7 +919,7 @@ export function standaloneProducerRunEffectFunction( return instance.run(standaloneProducerEffectsCreator, ...args); } else if (typeof input === "function") { - let instance = new AsyncState(nextKey(), input); + let instance = new AsyncState(nextKey(), input, {hideFromDevtools: true}); if (config?.payload) { instance.mergePayload(config.payload) } @@ -940,7 +940,7 @@ export function standaloneProducerRunpEffectFunction( return runWhileSubscribingToNextResolve(instance, props, args); } else if (typeof input === "function") { - let instance = new AsyncState(nextKey(), input); + let instance = new AsyncState(nextKey(), input, {hideFromDevtools: true}); if (config?.payload) { instance.mergePayload(config.payload); } From 040259d2f5afa9c1f95a944a58da173477df130f Mon Sep 17 00:00:00 2001 From: Mohamed EL AYADI Date: Fri, 18 Nov 2022 13:13:14 +0100 Subject: [PATCH 2/6] start better readme --- README.MD | 142 +++++++++++++++++++++++++------ packages/ts-example/src/main.tsx | 2 +- 2 files changed, 118 insertions(+), 26 deletions(-) diff --git a/README.MD b/README.MD index 5c51fdb4..d58bce8b 100644 --- a/README.MD +++ b/README.MD @@ -1,35 +1,127 @@ # React async states ## What is this ? -This is a multi-paradigm library for decentralized state management in React. -It aims to facilitate and automate working with [a]synchronous states while -sharing them. It was designed to reduce the needed boilerplate to -achieve great and effective results. +This is a multi-paradigm library for state management. + +It aims to facilitate working with [a]synchronous states while sharing them. +It was designed to reduce the needed boilerplate to achieve great and effective +results. + +It introduces a new concept: the `producer` that is similar to reducer, async +reducer or query from other libraries you might know, but with more power. + +This library provides utilities for low level state manipulation, and other +libraries may appear just a small abstraction on top of it. ## Main features The features that make this library special are: -- Easy to use and Minimal API (`useAsyncState`). -- Tiny library with 0 dependencies, only react as a peer dependency. -- Targets all react/javascript environments. -- Cancellations friendly (`props.onAbort(cb)`) -- Designed to support concurrency -- Supports many forms on functions (async/await, promises, generators, reducers) -- Run [side] effects either declaratively or imperatively. -- Built-in `status` in the state (initial, pending, success, error and aborted). -- Built-in `debounce` and `throttle`. -- Cache support. -- Events support. -- Dynamic creation and sharing of states at runtime. -- Share states inside and outside the provider without store. -- Subscribe and react to `selected` portions of state while controlling -when to re-render. -- Fork the state to have same behavior with separate subscribers. -- Lanes support to have several `grouped` states sharing the same cache. -- Hoist states to provider on demand at runtime. -- Automatic cleanup/reset on dependencies change (includes unmount). -- React 18+ friendly (already supported through the `read()` API). -- Powerful selectors. +### Easy to use and Minimal API (`useAsyncState`). +The library has one main hook: `useAsyncState` which allows the creation, +subscription and manipulation of the desired state. +[Here is a sneak peek](https://incepter.github.io/react-async-states/docs/api/the-whole-api#useasyncstate) +at this hook full API. + +### Tiny library with no dependencies and works in all environments +The library has no dependencies and very small on size compared to all the power +it gives, and it should target all environments (browser, node, native...). + +### Synchronous and asynchronous; Imperative and declarative support +The library adds the `status` property as part of the state, the possible values +are: `initial`, `pending`, `success`, `error` and `aborted`. + +When your producer runs, it becomes asynchronous if the returned value is a +`Thenable` object. But, you can control the `pending` status: eg, skip it +totally if our promise resolves under `400ms`. Or skip it entirely if you want +to perform some `fetch-then-render` patterns. + +The library allows you to perform declarative runs using `useAsyncState` +hook configuration, while also providing a multiple imperative `run` functions +with different signatures to answer your needs. + +### Promises, async/await & generators support +The `producer`, the core concept of the library can be of different forms (you +can even omit it and manipulate the state directly, without a producer function): + +Either return a promise (thenable) to your state, use async/await syntax or go +generators. All of these are supported by the library out of the box and +no configuration is needed. + +```typescript +useAsyncState(); +useAsyncState(function getSomeData() { return fetchMyData(); }); +useAsyncState(function* someGenerator() { yield fetchMyData(); }); +useAsyncState(async function getSomeData() { return await fetchMyData(); }); +``` + +[Here is a sneak peek](https://incepter.github.io/react-async-states/docs/api/the-whole-api#producer) at the producer signature: + +### Automatic and friendly cancellations +The library was designed from the start to support cancellations in a standard +way: an `onAbort` callback registration function that registers your callbacks, +that are invoked once your run is cancelled (either decoratively or imperatively). + +In practice, we found ourselves writing the following, depending on context: +```typescript +onAbort((reason) => controller.abort(reason)); +onAbort(() => socket.disconnect()); +onAbort(() => worker.terminate()); +onAbort(() => clearInterval(id)); +onAbort(() => clearTimeout(id)); +``` + +When your state loses all the subscriptions (and depending on the `resetStateOnDispose`) +configuration, it will go back to its initial state and aborting any ongoing run. +This behavior is opt-in, and it is not the default mode of the library. + +### Events and callbacks support +The library supports two forms of imperative notifications when state is updated: + +- Via `events` as a configuration of `useAsyncState`: This allows you to react +to updates occurring in a share piece of state. +- Via `runc` function: It allows having callbacks `per run`, not by subscription. + +```typescript +import {useAsyncState} from "react-async-states"; + +const {runc} = useAsyncState({ + // ... config + events: { + change: [ + newState => console.log('state changed'), // will be invoked every state change + { + status: 'success', // handler will be invoked only in success status + handler : (successState) => {}, + } + ], + } +}) + +// or per run callbacks: +runc({ + args: myOptionalArgs, + onError : () => {}, + onSuccess : () => {}, + onAborted : () => {}, // not called when the abort status is bailed out + // no onPending callback. +}); +``` + +### Dynamic creation and sharing of states at runtime + + +### Works with or without a provider + +### Apply effects or runs: debounce, throttle... + +### On-demand cache support + +### Forks and lanes support + +### Powerful selectors + +### And many more + ## Get started diff --git a/packages/ts-example/src/main.tsx b/packages/ts-example/src/main.tsx index b016a27a..7f418b6d 100644 --- a/packages/ts-example/src/main.tsx +++ b/packages/ts-example/src/main.tsx @@ -1,11 +1,11 @@ import React from "react"; import ReactDOM from "react-dom/client"; -import {autoConfigureDevtools} from "async-states-devtools" import EntryPoint from "./entryPoint"; import "./styles/index.css"; import {createSource} from "react-async-states/src"; +import {autoConfigureDevtools} from "async-states-devtools" autoConfigureDevtools({open: true}); createSource("demo", null, {initialValue: 0}); From b30c748f31e463cd2262571cf0c736bfb034a48c Mon Sep 17 00:00:00 2001 From: Mohamed EL AYADI Date: Sat, 19 Nov 2022 12:15:26 +0100 Subject: [PATCH 3/6] more on readme --- README.MD | 153 ++++++++++++++++++++--- packages/devtools-extension/package.json | 4 +- packages/react-async-states/package.json | 2 +- packages/ts-example/package.json | 2 +- 4 files changed, 138 insertions(+), 23 deletions(-) diff --git a/README.MD b/README.MD index d58bce8b..6067d40b 100644 --- a/README.MD +++ b/README.MD @@ -16,11 +16,24 @@ libraries may appear just a small abstraction on top of it. ## Main features The features that make this library special are: +### Multi-paradigm nature +The library can work with the following modes: + +- `Imperative` and/or `declarative` +- `Synchronous` and/or `Asynchronous` +- Data fetching and/or any form of asynchrony +- Inside and/or outside `React` +- Inside and/or outside `React context provider` +- With or without `Cache` +- `Promises`, `async/await` and even `generators` or nothing at all +- Allows abstractions on top of it +- ... + ### Easy to use and Minimal API (`useAsyncState`). The library has one main hook: `useAsyncState` which allows the creation, subscription and manipulation of the desired state. [Here is a sneak peek](https://incepter.github.io/react-async-states/docs/api/the-whole-api#useasyncstate) -at this hook full API. +at this hook's full API. ### Tiny library with no dependencies and works in all environments The library has no dependencies and very small on size compared to all the power @@ -108,25 +121,136 @@ runc({ ``` ### Dynamic creation and sharing of states at runtime +Under the `AsyncStateProvider`, you can create and share state instances +and access them by their `key` via the `hoistToProvider` option. +You can even start listening to a state before it gets hoisted to the provider, +and get notified once it gets added. ### Works with or without a provider +The library can work without the provider and still share state via the +`source` special object. ### Apply effects or runs: debounce, throttle... +To avoid creating additional state pieces and third party utilities, +the library has out-of-the box support for effects that can be applied to runs: +such as `debounce`, and `throttle` and `delay`. +This support allows you to create awesome user experience natively with the +minimum CPU and RAM fingerprints, without additional libraries or managed +variables. It just works in the core of the library. Of course, this requires +you to be in an environment where `setTimeout` exists. + +```tsx +import {useAsyncState, ProducerRunEffects} from "react-async-states"; + +const {run} = useAsyncState({ + producer: userSearchByUsername, + // debounce runs + runEffect: ProducerRunEffects.debounce, + runEffectDurationMs: 300, + // skip pending status if it answers less than 200ms + skipPendingDelayMs: 200, +}); + + + run(e.target.value)} /* ... */ /> +``` ### On-demand cache support +The library has a different cache support: it doesn't cache the value of you state, +rather, it caches your producer runs when they succeed by hashing the run `args` +and `payload`. + +Let's add cache support to the previous example: + +```tsx +import {useAsyncState, ProducerRunEffects} from "react-async-states"; + +// note that the whole configuration object does not depend on render +// and can be moved to module level static object. +const {run} = useAsyncState({ + producer: userSearchByUsername, + // debounce runs + runEffect: ProducerRunEffects.debounce, + runEffectDurationMs: 300, + // skip pending status if it answers less than 200ms + skipPendingDelayMs: 200, + + // cache config: + cacheConfig: { + enabled: true, // enable cache + // run cache hash is the username passed to the producer, this allows to + // have cached entries such as: `incepter` : { state: {data}} + hash: (args) => args[0], + getDeadline: (state) => state.data.maxAge || Infinity, + } +}); + + + run(e.target.value)} /* ... */ /> +``` + +The library allows you also to `persist` and `load` cache, even asynchronously +and then do something in the `onCacheLoad` event. ### Forks and lanes support +Forking a state in the library means having a new state instance, with the same +producer, and probably the same cache (configurable), while having a new isolated +state with new subscribers. + +The library has two ways for forks +- Normal forks: obtained by adding `fork: true` to `useAsyncState`, and these +are standalone states. +- Lanes: These are normal forks, but managed by their parent and share the same +cache, they can be enumerated from their parent via `source.getAllLanes`, +and removed by `source.removeLane`. + +```typescript +import {useAsyncState, useSourceLane} from "react-async-states"; + +const references = createSource("refs", referencesProducer, { + /* awesome config */ +}); + +const {state} = useAsyncState({ + source: references, + lane: 'cities', + lazy: false +}); +const {state} = useAsyncState({source: references, lane: 'roles', lazy: false}); +// can be simplified to this: +const {state} = useSourceLane(references, 'roles'); + +// re-use a state with its producer called weather present in the provider +const {state: weatherState} = useAsyncState({key: "weather", fork: true}); + +``` ### Powerful selectors +The library has two ways to select data from states: +- via `useAsyncState`: it supports a `selector` configuration can accept the +current state and the whole cache (you can decide to just work with cache, if you want to!) +- via `useSelector`: This hook allows you to select data from one or multiple +pieces of states, it even allows combining `keys` and `source` object to select from them. +It also can dynamically select states from the provider as they get hoisted. + ### And many more +The previous examples are just a few subset of the library's power, there are +several other unique features like: -## Get started +- Cascade runs and cancellations +- Run and wait for resolve +- Producer states that emit updates after resolve (such as websockets) +- Configurable state disposal and garbage collection +- React 18 support, and no tearing even without `useSES` +- StateBoundary and support for all three `render strategies` +- post subscribe and change events +- And many more.. -To get started using the library, please make sure to read [the docs](https://incepter.github.io/react-async-states/docs/intro). -[The tutorial section](https://incepter.github.io/react-async-states/docs/tutorial/first-steps) is a good starting point to get your hands dirty. + +## Get started The library is available as a package on NPM for use with a module bundler or in a Node application: @@ -136,25 +260,14 @@ npm install react-async-states # YARN yarn add react-async-states -``` - -## Use cases - -The library supports several paradigms, which allows it to support almost -every use case you can think of. +# PNPM +pnpm add react-async-states +``` -In a nutshell, the library can manage: -- Synchronous and asynchronous states -- Cancellations -- State sharing -- Selectors -- Caching -- Inside and outside provider -- Run effects such as debounce and throttle -- Events +To get started using the library, please make sure to read [the docs](https://incepter.github.io/react-async-states/docs/intro). +[The tutorial section](https://incepter.github.io/react-async-states/docs/tutorial/first-steps) is a good starting point to get your hands dirty. -[This section of the docs](https://incepter.github.io/react-async-states/docs/use-cases/) tells more about this. ## Contribution diff --git a/packages/devtools-extension/package.json b/packages/devtools-extension/package.json index 8e509add..5a4b3112 100644 --- a/packages/devtools-extension/package.json +++ b/packages/devtools-extension/package.json @@ -19,12 +19,14 @@ "react-json-view": "^1.21.3", "react-resizable": "^3.0.4" }, + "peerDependencies": { + "react-async-states": ">=1.0.0-rc-8" + }, "devDependencies": { "@types/node": "^18.11.9", "@types/react": "^18.0.24", "@types/react-dom": "^18.0.8", "@vitejs/plugin-react": "^2.2.0", - "react-async-states": "1.0.0-rc-7", "rollup-plugin-copy": "^3.4.0", "typescript": "^4.6.4", "vite": "^3.2.3", diff --git a/packages/react-async-states/package.json b/packages/react-async-states/package.json index 5e80fcfd..b1e26166 100644 --- a/packages/react-async-states/package.json +++ b/packages/react-async-states/package.json @@ -3,7 +3,7 @@ "license": "MIT", "author": "incepter", "sideEffects": false, - "version": "1.0.0-rc-7", + "version": "1.0.0-rc-8", "module": "dist/index", "main": "dist/umd/index", "types": "dist/src/index", diff --git a/packages/ts-example/package.json b/packages/ts-example/package.json index f460ea63..9736df93 100644 --- a/packages/ts-example/package.json +++ b/packages/ts-example/package.json @@ -16,7 +16,7 @@ "react": "18.2.0", "react-dom": "18.2.0", "react-router-dom": "6.4.3", - "react-async-states": "1.0.0-rc-7", + "react-async-states": "1.0.0-rc-8", "ts-node": "10.9.1" }, "devDependencies": { From 7d3f50e84e30458d249931333f131266a228722f Mon Sep 17 00:00:00 2001 From: Mohamed EL AYADI Date: Sat, 19 Nov 2022 12:21:54 +0100 Subject: [PATCH 4/6] fix readme --- README.MD | 35 +++++++++++++----------- packages/devtools-extension/package.json | 2 +- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/README.MD b/README.MD index 6067d40b..754c85ed 100644 --- a/README.MD +++ b/README.MD @@ -14,9 +14,8 @@ This library provides utilities for low level state manipulation, and other libraries may appear just a small abstraction on top of it. ## Main features -The features that make this library special are: -### Multi-paradigm nature +#### Multi-paradigm nature The library can work with the following modes: - `Imperative` and/or `declarative` @@ -29,17 +28,17 @@ The library can work with the following modes: - Allows abstractions on top of it - ... -### Easy to use and Minimal API (`useAsyncState`). +#### Easy to use and Minimal API (`useAsyncState`). The library has one main hook: `useAsyncState` which allows the creation, subscription and manipulation of the desired state. [Here is a sneak peek](https://incepter.github.io/react-async-states/docs/api/the-whole-api#useasyncstate) at this hook's full API. -### Tiny library with no dependencies and works in all environments +#### Tiny library with no dependencies and works in all environments The library has no dependencies and very small on size compared to all the power it gives, and it should target all environments (browser, node, native...). -### Synchronous and asynchronous; Imperative and declarative support +#### Synchronous and asynchronous; Imperative and declarative support The library adds the `status` property as part of the state, the possible values are: `initial`, `pending`, `success`, `error` and `aborted`. @@ -52,7 +51,7 @@ The library allows you to perform declarative runs using `useAsyncState` hook configuration, while also providing a multiple imperative `run` functions with different signatures to answer your needs. -### Promises, async/await & generators support +#### Promises, async/await & generators support The `producer`, the core concept of the library can be of different forms (you can even omit it and manipulate the state directly, without a producer function): @@ -69,7 +68,7 @@ useAsyncState(async function getSomeData() { return await fetchMyData(); }); [Here is a sneak peek](https://incepter.github.io/react-async-states/docs/api/the-whole-api#producer) at the producer signature: -### Automatic and friendly cancellations +#### Automatic and friendly cancellations The library was designed from the start to support cancellations in a standard way: an `onAbort` callback registration function that registers your callbacks, that are invoked once your run is cancelled (either decoratively or imperatively). @@ -87,7 +86,7 @@ When your state loses all the subscriptions (and depending on the `resetStateOnD configuration, it will go back to its initial state and aborting any ongoing run. This behavior is opt-in, and it is not the default mode of the library. -### Events and callbacks support +#### Events and callbacks support The library supports two forms of imperative notifications when state is updated: - Via `events` as a configuration of `useAsyncState`: This allows you to react @@ -120,18 +119,18 @@ runc({ }); ``` -### Dynamic creation and sharing of states at runtime +#### Dynamic creation and sharing of states at runtime Under the `AsyncStateProvider`, you can create and share state instances and access them by their `key` via the `hoistToProvider` option. You can even start listening to a state before it gets hoisted to the provider, and get notified once it gets added. -### Works with or without a provider +#### Works with or without a provider The library can work without the provider and still share state via the `source` special object. -### Apply effects or runs: debounce, throttle... +#### Apply effects or runs: debounce, throttle... To avoid creating additional state pieces and third party utilities, the library has out-of-the box support for effects that can be applied to runs: such as `debounce`, and `throttle` and `delay`. @@ -156,7 +155,7 @@ const {run} = useAsyncState({ run(e.target.value)} /* ... */ /> ``` -### On-demand cache support +#### On-demand cache support The library has a different cache support: it doesn't cache the value of you state, rather, it caches your producer runs when they succeed by hashing the run `args` and `payload`. @@ -193,7 +192,7 @@ const {run} = useAsyncState({ The library allows you also to `persist` and `load` cache, even asynchronously and then do something in the `onCacheLoad` event. -### Forks and lanes support +#### Forks and lanes support Forking a state in the library means having a new state instance, with the same producer, and probably the same cache (configurable), while having a new isolated state with new subscribers. @@ -226,7 +225,7 @@ const {state: weatherState} = useAsyncState({key: "weather", fork: true}); ``` -### Powerful selectors +#### Powerful selectors The library has two ways to select data from states: - via `useAsyncState`: it supports a `selector` configuration can accept the @@ -235,7 +234,7 @@ current state and the whole cache (you can decide to just work with cache, if yo pieces of states, it even allows combining `keys` and `source` object to select from them. It also can dynamically select states from the provider as they get hoisted. -### And many more +#### And many more The previous examples are just a few subset of the library's power, there are several other unique features like: @@ -254,13 +253,17 @@ several other unique features like: The library is available as a package on NPM for use with a module bundler or in a Node application: -```shell +```bash # NPM npm install react-async-states +``` +```bash # YARN yarn add react-async-states +``` +```bash # PNPM pnpm add react-async-states ``` diff --git a/packages/devtools-extension/package.json b/packages/devtools-extension/package.json index 5a4b3112..3dd45b13 100644 --- a/packages/devtools-extension/package.json +++ b/packages/devtools-extension/package.json @@ -2,7 +2,7 @@ "name": "async-states-devtools", "private": true, "type": "module", - "version": "0.0.0", + "version": "0.0.1", "types": "dist/index", "module": "dist/index.js", "main": "dist/index.umd.cjs", From e2e2ba9de79329f45cf90de67521e5d2d52fc96a Mon Sep 17 00:00:00 2001 From: Mohamed EL AYADI Date: Sat, 19 Nov 2022 12:30:06 +0100 Subject: [PATCH 5/6] fix readme, again --- README.MD | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.MD b/README.MD index 754c85ed..e6d45030 100644 --- a/README.MD +++ b/README.MD @@ -275,3 +275,6 @@ To get started using the library, please make sure to read [the docs](https://in ## Contribution To contribute, please refer take a look at [the issues section](https://github.com/incepter/react-async-states/issues). + + +By [@incepter](https://twitter.com/incepterr), with 💜 From cab0acaa73233a90ccc7d91d006c49a74deeda67 Mon Sep 17 00:00:00 2001 From: Mohamed EL AYADI Date: Sat, 19 Nov 2022 13:46:11 +0100 Subject: [PATCH 6/6] fix devtools lib build globals --- packages/devtools-extension/vite.config.lib.ts | 3 ++- .../react-async-states/rollup/rollup.config.js | 18 +++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/devtools-extension/vite.config.lib.ts b/packages/devtools-extension/vite.config.lib.ts index 818445b2..d12a03af 100644 --- a/packages/devtools-extension/vite.config.lib.ts +++ b/packages/devtools-extension/vite.config.lib.ts @@ -13,11 +13,12 @@ export default defineConfig({ fileName: 'index' }, rollupOptions: { - external: ['react', 'react/jsx-runtime'], + external: ['react', 'react/jsx-runtime', 'react-async-states'], output: { globals: { react: 'React', 'react/jsx-runtime': 'jsxRuntime', + 'react-async-states': 'ReactAsyncStates', } } }, diff --git a/packages/react-async-states/rollup/rollup.config.js b/packages/react-async-states/rollup/rollup.config.js index f98a9701..6e273261 100644 --- a/packages/react-async-states/rollup/rollup.config.js +++ b/packages/react-async-states/rollup/rollup.config.js @@ -261,15 +261,15 @@ const devtoolsSharedBuild = [ commonjs(), terser(), - copy({ - hook: 'closeBundle', - targets: [ - { - dest: 'dist/devtools/view', - src: `../devtools-extension/dist/*`, - }, - ] - }), + // copy({ + // hook: 'closeBundle', + // targets: [ + // { + // dest: 'dist/devtools/view', + // src: `../devtools-extension/dist/*`, + // }, + // ] + // }), ] } ];