3. async-states: The source object
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:
- async-states: the state management library
- async-states: Using react
- async-states: The source object
- async-states: The producer
- async-states: Advanced concepts
- async-states: vs react-query vs redux vs recoil
- 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:
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:
Note: CreateSource
has two signatures
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:
Note: setState
doesn’t run the producer and will abort any ongoing run.
Here are some examples of its usages:
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);
:
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).
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.
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:
Subscribe to updates
The library also provides a subscribe
function that allows you to listen to state updates:
Attach events listeners to state instance
The library adds an event system that you can use as follows:
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:
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 effectautoRunArgs: any[]
The args to pass to producer in case ofautoRun
condition: boolean | ((currentState) => boolean)
: To decide whether to auto run or notpayload: Record<string, any>
: the payload to be merged in the subscribed instancewait: boolean
: ordersuseAsyncState
to wait for a state if not existent rather than creating itfork: boolean
whether to create a separate instance from the provided by the configurationforkConfig
: 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 devtoolsevents
A configuration object for{change, subscribe}
events, read more about them here in the docs.selector(currentState, cache): DerivedState
: the returnedstate
fromuseAsyncState
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 ofselector
if present)read(suspend: boolean = true, throwError: boolean = true)
: returns thestate
and throws aPromise
ifpending
ordata
iferror
(optimization for concurrent feautes and error boundaries)version:number
the current captured version of the state instancelastSuccess
the last succeeded valueflags: number
source
our source objectdevFlags
decodedflags
array when in development modeonChange()
allows to registerchange
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:
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.