diff --git a/README.MD b/README.MD index 5c51fdb4..e6d45030 100644 --- a/README.MD +++ b/README.MD @@ -1,69 +1,280 @@ # 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. -## Get started +#### Multi-paradigm nature +The library can work with the following modes: -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. +- `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'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 +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 +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: + +- 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.. + + +## Get started 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 +``` -## Use cases - -The library supports several paradigms, which allows it to support almost -every use case you can think of. - -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 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 💜 diff --git a/packages/devtools-extension/package.json b/packages/devtools-extension/package.json index 8e509add..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", @@ -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/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/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/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/*`, + // }, + // ] + // }), ] } ]; 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); } 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": { 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});