xr 0.0.1
xr: ^0.0.1 copied to clipboard
XR is a simple state management library based on fundamental ideas of Computer Science
XR #
XR is a simple state management library. Idea is to go back to the fundamentals. It is XR because it is the opposite of Rx which a lot of "reactive" libraries use.
Why "state management"? #
Why do we need state management? When some piece of data changes, we want the relevant UI to also change; more generally: when something happens, we want to be able to "react" to it. It may be making an API call or updating the shared preferences or whatever or producing new state that the rest of the code uses.
Why yet another state management library #
I wanted to build a library that -
- Is based on fundamental ideas of computer science
- Has 0 dependencies
- Is declarative as far as possible
- Ideally a single file that can be copied and pasted into the codebase if required.
- For fun?
Fundamental Theory #
In any application, fundamentally what we are doing is reacting to events. When button is clicked, show this text, when this other button is clicked, open this page. Sometimes, it is a chain, when user clicks on login call the API. When the API is successful, navigate to the home page, and so on.
If we see, what we have is a Graph. Each "event" is a node, and the adjacent nodes are what we want to execute when the event happens. Then, reactivity is just the ability to traverse this graph from a starting node when something changes. But we dont want arbitrary graphs. What we want is a directed acyclic graph
It is directed because if Node A depends on B, does not imply Node B has any relation with A. And it should be acyclic because if there are cycles, the traversal will never end.
The keen eyed folks must have noticed, what we want is an even special case of graphs. What we want is a Mealy Machine (except cycles because we do want the computation to terminate :P). The new state is computed with the current state and the input together.
What happens when something changes? #
When an event triggers i.e. something changes, we schedule a microtask (if not already scheduled). The microtask creates a topological sort of the graph and allows the updated nodes to get their new value in this sorted order.
Features #
TODO: List features
Getting started #
TODO: Add Getting Started
Usage #
XR has two fundamental concepts -
- Events and
- Reactions
Ideally, we would only have one but I wanted very different APIs for the two.
- Event - Events represent a value that can be changed. This can be things
like button click, navigation, etc. You need to set their values manually using
setValue. You should only use reactions for things that cannot be computed; like button clicks. - Reaction - This is what makes the library reactive. Reaction is a pair of two things - a function that returns the current value and a set of dependencies. Reactions are lazy. The function is only called when at least one dependency changes. Dependency can be either other reactions or events. Most of the time, you should prefer Reactions.
void main () async {
final one = Event<int>(0); /// All events need to have an initial value
final two = Reaction<int>((_) { /// callback function that returns current value
/// Raw value is a bad idea but here, we know for a fact that one is initialized.
/// So, it is okay to use here. Read below to find out why
return one.rawValue + 2;
}, {one}); /// dependencies. two depends on one
final three = Reaction<int>((_) {
/// Raw value is a bad idea but here, we know for a fact that one is initialized.
/// So, it is okay to use here. Read below to find out why
return one.rawValue + 3;
}, {one}); /// three also depends on one
final four = Reaction<int>((_) {
/// Raw value is a bad idea but here, we know for a fact that one is initialized.
/// So, it is okay to use here. Read below to find out why
return two.rawValue + three.rawValue;
}, {two, three}); /// four depends on two and three
/// Set value sets an internal field and marks the node as changed
/// but the dependency resolution happens in a microtask. So waiting for
/// the future to complete
one.setValue(1);
await Future.value();
expect(one.currentValue, equals(1));
expect(one.value, equals(1));
/// After first future is complete, it adds 2 more nodes - two and three as
/// changed and schedules another task to update them. So wait for another
/// future
await Future.value();
expect(two.value, equals(3)); /// 1 + 2 = 3
expect(three.value, equals(4)); /// 1 + 3 = 4
/// After 2 and 3 are completed, they will mark 4 as changed and schedule
/// another task to update it
await Future.value();
expect(four.value, equals(7)); /// 3 + 4 = 7
}
Do you see the Directed Acyclic Graph?
But, we have a Mealy machine. Mealy machine can update state based on not just on the change but also current State. How to access current state?
Inside the Reaction callback, there is one parameter - the Reaction itself. So you can do something like:
final one = Reaction((reaction) {
return reaction.value.when(
empty: () => -1,
value: (v) => v + 1,
);
});
Note: There is an important distinction between Event and Reaction: Event always has a value. But, Reaction is lazy. The callback is never called if a dependency does not change. Hence, it does not have a value the first time the callback is called.
How to access value #
Reaction #
For Reaction, there are two ways to access value -
final one = Reaction((reaction) {
return reaction.value.when(
empty: () => -1,
value: (v) => v + 1,
);
/// or
return reaction.rawValue + 1;
});
value is a monadic optional. It will make sure you handle the first case
where the reaction does not yet have a value. But, if you are really sure,
that the value is set, you can use rawValue. rawValue basically throws an
error if optional is empty.
Event #
For event, in addition to the monadic value, and rawValue, there is a
currentValue. When you call event.setValue(), it changes the currentValue.
So currentValue will have value that might not have been sent to others yet.
So, it is best to not use it unless you know what you are doing.
Also, note: For Event, rawValue will never throw because it always has a
value. You give it the initial value upon construction and when you setValue,
you always have a value as there is no way to mark value to be empty.