In previous chapter we have implemented a basic synchronous state node, which can emit events using thennable object.
It would be nice if our state management library provides some solution for combining multiple state nodes into a single value, which would be especially helpful with decoupled data. In many libraries this tool is called selector
, thus let's just go with this name.
Reminder: this tutorial series describe how to create Awai state management library from scratch.
This part will be quite easy and understandable, so grab your coffee and relax before we dive into an async state management in further parts.
Selector will accept an array of state nodes and predicate function, which will be used to combine a result. Selector should react to any state changes, and determine new value each time changed event is emitted. All the state nodes' values will be passed to predicate as arguments preserving their order in an array.
Let's write selector function scaffold, and try to stick to our sync state node interface.
const selector = (states, predicate) => {
const initialValue = predicate(...states.map(state => state.get()));
let value = initialValue;
const events = {
changed: new AwaiEvent()
};
const get = () => value;
return { events, get };
};
It looks nice already. We're calling predicate in order to determine an initialValue
, and we even expose events. But we do not react to any dependency changes. Let's assume we're in The Stone Age, and Scenario has not yet been discovered. What can we do? As you remember, all our events are thennable (promise-like). So let's use this fact to create an async loop, which will watch these events.
const listenToChanges = async () => {
await Promise.any(states.map(state => state.events.changed));
const statesValues = states.map(state => state.get());
value = predicate(...statesValues);
events.changed.emit(value);
listenToChanges();
};
That's the hardest piece of code in this part. Here we await a promise which is a combination of all of nodes' changed events - as soon as any of them is emitted (resolved), we call the predicate function to determine new result and store it in value
variable. Right after that we emit changed event from the selector. And finally call the same function to start watching changes again.
Complete util code:
const selector = (states, predicate) => {
const initialValue = predicate(...states.map(state => state.get()));
let value = initialValue;
const events = {
changed: new AwaiEvent()
};
const get = () => value;
const listenToChanges = async () => {
await Promise.any(states.map(state => state.events.changed));
const statesValues = states.map(state => state.get());
value = predicate(...statesValues);
events.changed.emit(value);
listenToChanges();
};
listenToChanges();
return { events, get };
};
Now we can use this helper like this:
const nameState = state('John');
const ageState = state(30);
const userDetailsState = selector(
[nameState, ageState],
(name, age) => ({ name, age })
);
console.log(userDetailsState.get()); // { name: 'John', age: 30 }
Summary
We have created a very useful helper - Selector, which allows to combine multiple state nodes into a single value. So far it only works with sync states, but as we proceed with this tutorial, we would have to support async as well.
You can check our selector in action at the playground.
See you in next chapters!
Top comments (0)