1. Introducing async-states, The state management library

Mohamed EL AYADI
7 min readFeb 20, 2023

This post serves as first introduction to the state management toolbox: async-states. You will get an overall idea about how it works, how to use it and the API it offers.

This post is a part of the following blog post series:

  1. async-states: the state management library
  2. async-states: Using react
  3. async-states: The source object
  4. async-states: The producer
  5. async-states: Advanced concepts
  6. async-states: vs react-query vs redux vs recoil
  7. react-async-states: SSR

Plan

  • What is this ?
  • Motivations to build this library
  • Glimpse of the library’s power
  • Library primitives and concepts
  • Documentation and usage examples

What is this ?

async-states is a state management toolbox that works wherever you have a JavaScript runtime (it can be the browser, the server, workers…).

This library was designed to reduce the boilerplate needed to access and manipulate state in a given application: you may take full control over a state from anywhere in your app.

The key features of the library are:

  • Works literally anywhere
  • Minimal bundle size and memory fingerprint
  • Synchronous and asynchronous flows: Promises, async/await, generators, or even nothing
  • Very rich API that will support all your use cases (even subscriptions, websockets, timeouts, intervals…)
  • Opt-in features like cache, retry, debounce and throttle …

You can get an overall idea of the features in the library’s documentation.

Motivations to build this library

There are several state managers out there, some say that a state manager is born every time you yawn, but not all state managers are created equal (All hail async-states).

All state managers … are not created equal!

I’ve been in the puzzle of Javascript and state management for almost a decade now, and with react for 7 years, so I’ve basically seen(except for redux) all others being born and how they evolved over time. Yet, I always feel that something is missing in the puzzle.

I started writing code back to 2010 and I used many languages and platforms, each one of has its own specifications, APIs and limitations. In the last years, I wrote A LOT of enterprise applications for different businesses and corporates, mainly using Java and JavaScript. This allowed me not only to have clear vision about the API to design and craft, but also how to push it to solve all the problems I once had manipulating state with UI.

So Basically I want:

  • Less code and more features
  • Consistent and effective state management, sync or async
  • Cancel any asynchronous flow, automatically and/or imperatively
  • Generators support for “true” cancellations
  • On-demand Cache and retry support, and even cache mutualization
  • Effects such as debounce and throttle
  • Subscribe and manipulate any state, from anywhere
  • Skip and tune pending transition behavior
  • And more importantly, all the previous aspects should work together!

Follow along to see how async-statesis unique.

Glimpse of the library’s power

Hold your breath; this is a debounced-cancellable-cached — with max-age cache control header — search-as-you-type that skips the pending state if the search answers in less than 300ms, and that stays in the pending state for at least 300ms to avoid UI flickering:

This is not for everyone!

Try it yourself in this codesandbox!

Let’s speak first about the properties used in the previous example:

  • key: The unique identifier of the state, you can connect to it from anywhere.
  • producer : This is the function that returns our state value (or throws it), and can be of any form and perform unbelievable tasks.
  • skipPendingDelayMs : If the request is fast enough, the pending status will be completely skipped.
  • keepPendingForMs : If your UI enters the pending state, keep that state for at least this duration, to avoid the UI flashing showing some indicator and immediately unmounting it if the request answers right after.
  • runEffect and runEffectDurationMs : The effect to apply on runs , can be debounce or throttle for now.
  • cacheConfig : The cache config to apply. If no getDeadline function is provided to tune the cache time per state, and if the data is an object like the Response object, the cache control header is attempted and maxAge would be taken automatically.

If you are into frontend development, you should know how delicate will be to implement all of these features to work together, and work consistently. But for the library, it doesn’t even depend on react or the render cycle.

This is the most basic usage of the library, and its power has nothing to do with this small example, take a look at the whole signature of useAsyncState if you are curious!

You can also use the core library with react directly: here is the previous example using useSyncExternalStore rather than useAsyncState:

async-states with useSES

Library primitives and concepts

Concepts

To use the library, you should get familiar with these concepts:

  • State : The state used and provided by the library contains 4 properties: status, timestamp, data and props. You can learn more about them in the docs.
  • The producer: The function responsible for getting the state value is called a producer: it produces the state value. It might be a synchronous function, a function returning a promise, async/await syntax, generators or even a nullish value to manipulate the state on the fly. Read more in the docs.
  • source: The source object is obtained from the library from several places, and it is a wrapper around the internal state instance of the library that allows full control: reading state, running producer, manipulating config, attaching events, cache replacement or even imperatively altering the state value. The most common way to create them is via createSource function. It can be called everywhere, and if the requested state (by key) already exists, it will be used.

The Source object

As of writing this post, the library’s source object has the following shape:

Source shape

Like mentioned before, this is a wrapper around an internal state instance with much more properties, almost all of them are opt-in and not allocated by default unless you use the feature, from payload to subscription to anything else.

The library understands this source object and can decode it to retrieve the original state instance if needed, it was made to prevent developers from manipulating the state instance directly, and only a subset is provided that basically allows anything.

The Producer

For the brevity of the post, I recommend checking the docs for the producer, we will here only discuss its signature and its power for now.

Here is the type definition of the producer:

Producer type

Where the ProducerProps are defined as:

Producer props

Read about them here.

The producer can:

  • Register abort callbacks to cleanup his work (abort fetch, terminate worker, clear timeout/interval, disconnect websocket…)
  • Retrieve some context for the current run from either args or payload
  • Access to the last succeeded state, this can be useful for any type of producers that use the previous succeeded state, like reducers and infinite lists.
  • Check if it was aborted to decide whether to continue work in a non-automatically abortable context such as promises or async/await syntax. The library has a “one” level generator-runner that automatically stops yielding once aborted.
  • Run and get the abort function of any other state that it has access to with the ability to perform cascading cancellations
  • Run and get a promise via runp
  • Run using callbacks when status change via runc
  • Select the current state of any other state by its key or source object
  • emit state updates after resolve; this is an optimization for subscription producers, like websockets or intervals for example: Unless there is a new run or abort was called explicitly, you can change the state from a producer by using the emit function without loosing that connexion.
  • abort its own run; if the producer didn’t step into the pending state when this function is called, it will be entirely bailed out.

If you’ve made it until here, congratulations! You should be aware of many of the library’s features.

Documentation and usage examples

The library can be used for many of your daily use cases;

Transitions

Data fetching

The most common use case is data fetching; you will find yourself writing a lot code like this:

Typical usage

State sharing and manipulation

The library can manage synchronous states as well either if the producer is omitted or if it doesn’t return a Promise.

If the producer isn’t provided, the run function will delegate the work the the setState function: It will update the state by the given value, if it was given a function, it will receive the current state. Consider the following example

Shared state

useAsyncState can receive just a state‘s (string) key as parameter and do the rest for you! Test the previous example here.

Conclusion

By now, I bet you have a great understanding of how the library may solve all of your daily problems and give you a great boost of productivity and confidence.

In the next section we will see how the library works very well with react and how much power it gives in practice.

--

--