A multi-paradigm React state management library.
This is a multi-paradigm library for state management.
It aims to facilitate working non only with asynchronous states, but only the synchronous ones. It was designed to reduce the need boilerplate to achieve great, predictable and effective results.
The library can work with the following modes:
Imperative
and/ordeclarative
Synchronous
and/orAsynchronous
- Data fetching and/or any form of asynchrony
- Inside and/or outside
React
- With or without
Cache
- sync,
Promises
,async/await
or nothing at all - Allows abstractions on top of it
- ...
The main hook of the library: useAsync
allows the creation,
subscription and manipulation of the desired state.
The hooks signature is pretty familiar: a configuration and dependencies.
useAsync(create, deps);
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...).
The library adds the status
property as part of the state, the possible values
are: initial
, pending
, success
and error
.
When your function runs, it becomes asynchronous if the returned value is a
promise
. Or else, it passes synchronously to the resolving state.
You can also control the pending
status by skipping it in certain cases: let's
say you API answered very fast (for example, less than 300ms) in this case
there will be no transition to the pending state. Of course, this is an opt-in
behavior via the skipPendingDelayMs
configuration.
The library was designed to look familiar to you. For example, the main hook that you will interact with has a signature similar to the hooks you've been using in React the whole time. ie:
const result = useAsync(options, dependencies);
The library default dependencies are an empty array for convenience.
The result given by the previous hook are also similar to other libraries you may have used before, such as react-query, redux toolkit query or apolo client:
const { isPending, data, error } = useAsync(config, deps);
Of course, the library has many other things that will cover all your needs.
The producer
, the core concept of the library can be of different forms:
It can be a sync function such as a variant of a reducer, or return a promise (thenable) to your state or use async/await syntax.
useAsync();
useAsync(function getSomeData() { return fetchMyData(); });
useAsync(async function getSomeData() { return await fetchMyData(); });
It is important to note that the previous writing won't trigger or run your
function, on the contrary to some other libraries that requires you to control
it via a flag. This library uses the lazy
flag, which defaults to
true. In order to make it automatic, you need to provide a lazy: false
option.
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 (or manually).
In practice, we found ourselves writing the following, depending on context:
onAbort(() => socket.disconnect());
onAbort(() => worker.terminate());
onAbort(() => clearInterval(id));
onAbort(() => clearTimeout(id));
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.
import { useAsync } from "react-async-states";
const { source: { run } } = useAsync({
producer: searchUserByName,
// debounce runs for 300ms
runEffect: "debounce",
runEffectDurationMs: 300,
// skip pending status under 200ms
skipPendingDelayMs: 200,
// stay in pending state for at least 500ms if you enter it
keepPendingForMs: 500,
});
<input onChange={e => run(e.target.value)} /* ... */ />
Obviously, there is cache. But that's opt-in via the cacheConfig.enabled
configuration to avoid unexpected behavior due to existing cache.
Let's add cache support to the previous example:
import { useAsync } 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 { source: { run } } = useAsync({
producer: searchUserByName,
// debounce runs for 300ms
runEffect: "debounce",
runEffectDurationMs: 300,
// skip pending status under 200ms
skipPendingDelayMs: 200,
// stay in pending state for at least 500ms if you enter it
keepPendingForMs: 500,
// cache config:
cacheConfig: {
// enable cache
enabled: true,
// 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],
// this is a successful state. Only success states are cached
// The library uses Infinity as a default cached timeout
timeout: (state) => state.data.maxAge || 60 * 5_000,
// automatically run the function again after the cache is expired
// this is not applicable to Infinity.
auto: true,
}
});
<input onChange={e => run(e.target.value)} /* ... */ />
The library allows you also to persist
and load
cache, even asynchronously
and even do something in the onCacheLoad
event.
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..
Managing state using React APIs or third party libraries ain't an easy task. Let's talk about the parts we miss:
- Share state in all directions of your app.
- Combining synchronous and asynchronous effects in a single library.
- Automatically reset a state when you no longer use it.
- Dealing with concurrent asynchronous operations' callbacks.
- Run functions with different arguments, anytime.
- Dynamically share states, subscribe and have full control over them.
- Select a part of a state and re-render only when you decide that it changed.
- Automatically cancel asynchronous operations when the component unmounts, or dependencies change.
The library is available as a package on NPM for use with a module bundler or in a Node application:
npm install async-states react-async-states
yarn add async-states react-async-states
pnpm add async-states react-async-states
To get started using the library, please make sure to read the docs. The tutorial section is a good starting point to get your hands dirty.
To contribute, please refer take a look at the issues section.
By @incepter, with 💜