3. async-states: The source object

Mohamed EL AYADI
7 min readFeb 20, 2023

--

This magical object sealing away the internal implementation of the library so developers won’t mess with it. In this post, we’ll know more about the source object.

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

  • Why ?
  • What it contains ?
  • How to use it?
  • How to use it with React ?
  • The return of useAsyncState

Why ?

async-states stores its state in an object in the memory, in a property called state, this obviously has nothing to do with react.

This property is frozen and hidden along with a bunch of other properties in the state instance, such as: subcsriptions, locks, latestRun, lanes and many other implementation details properties that:

  • Cannot be guarantied to exist in the public API for the long run
  • Should not be used other than by the library itself
  • Should not be manipulated or altered without the library’s knowing

So the idea was that the library will give the developer an object with some methods to manipulate the state instance, this object will allow the library itself to decode the real state instance for manipulation.

There is a nasty technique used by the library to achieve this, if you are curious, read it here!

The thing to retain is that the source is like an interface, and the class implementing this interface is the state instance. There is a one-to-one relationship between every state instance and its source object.

This will allow the library’s internals to follow another road that what the public API is offering! quit powerful.

What it contains ?

We’ve seen this before in Part 1 of this series:

The source object

How to use it

In this section, we will try to manipulate our state like React doesn’t exist, so you will know the power needed to make your state working everywhere.

Create state

The most straightforward way to create state instances in the library is via createSource. Here are some examples:

createSource basic examples

Note: CreateSource has two signatures

createSource overloads

So by now we created several shared states. Let’s change some values.

Set the state value

The basic way to change the state value is by calling the source.setState method:

setState signature

Note: setState doesn’t run the producer and will abort any ongoing run.

Here are some examples of its usages:

setState usage

Note: source.setState can be called everywhere, it will change the state value and then notify all subscribers.

Run the producer

To run the producer attached to a state, simply: source.run(...args); :

Runnin a state

Note: In case there is no producer attached to your state, the call to run will be entirely delegated to setState.

Change the producer

Changing the producer is allowed, although it isn’t always a great idea since you won’t be type-safe and a lot of issues. But it is allowed to be able to perform abstractions on the userland that may patch the producer at any update. Which should be okay in case the producer is created in a closure and will be type-stable (although, args and payload were invented to supress your need of doing this).

replaceProducer

Manipulate the payload

The payload being an internal object in the state instance, it can hold anything for you from all subscribers and prepare it to be consumed when the producer runs.

Using payload

Note: The payload isn’t reactive in the sense that you will be notified when it changes. A dedicated event to that may be added in the future.

Replay latest run

The source.replay() will replay the latest run with a shallow copy of the payload and args used in the latest run. You can use it for example when a request fails and you display a try again button to the user.

Run and get a promise to fulfillment

runp is a special function that accepts the args to be passed to the producer (just like run ) except it doesn’t return the abort function, but a Promise that resolves with whatever state resulted from your run:

usage of runp

Subscribe to updates

The library also provides a subscribe function that allows you to listen to state updates:

subscribe usage

Attach events listeners to state instance

The library adds an event system that you can use as follows:

state events

As you can see, .on returns a function that removes the attached event or events and can be used anywhere.

Note: Make sure you have a way to remove the event or it will be invoked whenever the event occurs.

Note 2: It may be possible in the future to add subscription events, which are events linked to a single subscription, that obviously get removed when unsubscribing.

Usage with React

Having the previous low level utilities, the react part become just a subscriber, but to allow a wide range of possible configurations, I introduced useAsyncState(create, deps) as a hook, the create parameter can be:

Nothing

In this case, an empty state is created and its key and source will be returned (along with all other properties).

let {state, setState} = useAsyncState()

A string

In this case, the library will either use the state with this string as key, if not found, it will be created.

Note: To wait for this state, use:

let {state, setState} = useAsyncState({
wait: true,
key: "my-key",
})

A source Object

You may have seen this already in this blog post series:

useAsyncState with source

A producer

Of course, you are able to pass directly a producer here! but pay attention if it contains any information from the render of your component, in this case, you should add your relevant ones to the dependencies array of useAsyncState.

A configuration object

The configuration object is detailed in the documentation, we’ll highlight it only here, it contains the following properties (all of them are optional):

  • key: string
  • producer: Producer
  • source : if the source is provided, it is used
  • All of the ProducerConfig properties: initialValue, skipPendignDelaysMs, cacheConfig, retryConfig ...
  • lazy: boolean if false, the subscription will run the producer on effect
  • autoRunArgs: any[] The args to pass to producer in case of autoRun
  • condition: boolean | ((currentState) => boolean) : To decide whether to auto run or not
  • payload: Record<string, any> : the payload to be merged in the subscribed instance
  • wait: boolean : orders useAsyncState to wait for a state if not existent rather than creating it
  • fork: boolean whether to create a separate instance from the provided by the configuration
  • forkConfig : configuration to apply when forking (sorry I didn’t talk about forks earlier, but they are in the docs and it will be discussed in advanced stuff).
  • subscriptionKey: string : The key of the subscription, will be visible in the devtools
  • events A configuration object for {change, subscribe} events, read more about them here in the docs.
  • selector(currentState, cache): DerivedState: the returned state from useAsyncState is the return of this selector.
  • areEqual(prev, next): boolean compares the current and next selected portion of the state whether to render or not.

The return of useAsyncState

useAsyncState‘s return value contains the all the properties that the source contains, plus:

  • state : The actual state (or the result of selector if present)
  • read(suspend: boolean = true, throwError: boolean = true) : returns the state and throws a Promise if pending or data if error (optimization for concurrent feautes and error boundaries)
  • version:number the current captured version of the state instance
  • lastSuccess the last succeeded value
  • flags: number
  • source our source object
  • devFlags decoded flags array when in development mode
  • onChange() allows to register change events for the current subscription. This is an optimization to add callbacks from render directly (they will be appended later).

If you like diagrams, here is how all this fit in:

useAsyncState diagram

Note: The library isn’t using classes for any of these, but types are mostly interfaces with inheritance.

The internal state instance inherits too from BaseSource from the previous diagram.

Conclusion

By no doubt, the source object is the magic in the library: It stops your from randomly manipulating the internal state instance while giving you full control over it, and it can also be used as a “state identifier” by the library.

The source usually is linked to the producer, which is another important brick in the library. In the next post, we will talk about the producer’s power and how to write good ones.

--

--