A state represents a value that can change, i.e. a reactive value (as in "it reacts to stuff"):
1linkimport { state } from 'rxdeep';
2link
3linkconst a = state(42);
4linkconst b = state([1, 2, 3, 4]);
5linkconst c = state({
6link name: 'Awesome Team',
7link people: [
8link { id: 101, name: 'Jeremy' },
9link { id: 102, name: 'Julia' }
10link ],
11link ...
12link})
You can also create states using the State
class constructor:
1linkimport { State } from 'rxdeep';
2link
3linkconst a = new State(42);
A state is an Observable
, so you can subscribe to it and read
its values as they change over time:
1linkconst a = state(42);
2linka.subscribe(console.log); // --> log values from a
3link
4linka.value = 43; // --> logs 43
5linka.value = 44; // --> logs 44
6linka.value = 44; // --> logs 44
7link
8link// Logs:
9link// > 42
10link// > 43
11link// > 44
12link// > 44
You can change the value of a state using its .value
property:
1linka.value = 44;
Or using its .next()
method:
1linka.next(44);
You can also read current value of a state using its .value
property:
1linkconst a = state(42);
2linka.subscribe(); // --> this is important!
3link
4linkconsole.log(a.value); // --> logs 42
5linka.value = 43;
6linkconsole.log(a.value); // --> logs 43
7link
8link// Logs:
9link// > 42
10link// > 43
warning WARNING
.value
will not be up to date with state's latest changes unless the state is subscribed to, which means either.subscribe()
method of the state should have been called or that of one of its sub-states (or proxy states).So this will NOT work:
1linkconst a = state(42);2linka.value = 43;3linkconsole.log(a.value); // --> logs 42!!!4link5link// Logs:6link// > 42
A state is also an Observer
, which means it can subscribe to
another Observable
:
1linkimport { interval } from 'rxjs';
2link
3linkconst a = new Subject(0);
4linkinterval(1000).subscribe(a);
ALWAYS make changes to the state that respect object immutability. You MUST always ensure that you are changing the reference when changing the value of a state.
This happens automatically with raw values (number
, boolean
, string
, etc.). For more complex values (objects and arrays),
use the rest operator ...
or methods that create a new reference.
DON'T:
1link/*~*/s~.~value~~~~~.~push~~~~(~x~)~/*~*/;
DO:
1links.value = s.value.concat(x);
2link// -- OR --
3links.next(s.value.concat(x));
DON'T:
1link/*~*/s~.~value~~~~~.~x ~~=~ ~42~~/*~*/;
DO:
1links.value = { ...s.value, x: 42 }
2link// -- OR --
3links.sub('x').value = 42;
4link// -- OR --
5links.next({ ...s.value, x : 42 });
6link// -- OR --
7links.sub('x').next(42);
Take this state:
1linkconst team = state({
2link name: 'Awesome Team',
3link people: [
4link { id: 101, name: 'Julia' },
5link { id: 102, name: 'Jeremy' },
6link ]
7link})
The whole team
is a value that changes over time, but so is its .name
, or its .people
. Also the .length
of the .name
is a value
that changes over time, so is the first person in .people
list.
You can represent all these reactive values with State
objects as well, using team
's .sub()
method:
1linkconst name = team.sub('name');
2link
3linkconst nameLength = name.sub('length');
4linkconst nameLength = team.sub('name').sub('length');
5link
6linkconst people = team.sub('people');
7link
8linkconst firstPerson = people.sub(0);
9linkconst firstPerson = team.sub('people').sub(0);
10link
11linkconst firstPersonsName = people.sub(0).sub('name');
12linkconst firstPersonsName = team.sub('people').sub(0).sub('name');
In this example, team
is the parent state of all (also called the root state), and name
is its sub-state (or child state).
nameLength
is also a sub-state of name
, you could call it a grandchild of team
.
You can use sub
method with any possible key (index, string key, symbol, etc) of the objects of the parent state. The
result is another state object, reflecting the state of that particular property.
Sub-states pick up changes made to their parent states:
1linkconst team = state({
2link name: 'Awesome Team',
3link people: [
4link { id: 101, name: 'Julia' },
5link { id: 102, name: 'Jeremy' },
6link ]
7link});
8link
9linkteam.sub('name').sub('length').subscribe(console.log);
10linkteam.value = {
11link name: 'That Other Team',
12link people: [...]
13link};
14link
15link// Logs:
16link// > 12
17link// > 15
A sub-state only emits values when its value has really changed:
1linkteam.sub('people').sub(0).sub('name').subscribe(console.log);
2link
3linkteam.value = {
4link name: team.value.name, // --> do not change the name
5link people: [
6link {id: 101, name: 'Julia'},
7link {id: 103, name: 'Jaber'},
8link {id: 102, name: 'Jeremy'},
9link ]
10link}
11link
12linkteam.sub('people').sub(0).value = { id: 104, name: 'Jin' }
13link
14link// Logs:
15link// > Julia
16link// > Jin
Or when a change is issued at the same address of the tree:
1linkteam.sub('people').sub(0).sub('name').subscribe(console.log);
2linkteam.sub('people').sub(0).sub('name').value = 'Julia';
3link
4link// Logs:
5link// > Julia
6link// > Julia
You can make changes in sub-states while listening to them on parent states:
1linkconst team = state({
2link name: 'Awesome Team',
3link people: [
4link { id: 101, name: 'Julia' },
5link { id: 102, name: 'Jeremy' },
6link ]
7link});
8link
9linkteam.subscribe(console.log);
10linkteam.sub('name').value = 'That Other Team';
11link
12link// Logs:
13link// > { name: 'Awesome Team', people: [...] }
14link// > { name: 'That Other Team', people: [...] }
touch_app IMPORTANT
It is a good idea to ensure sub-states are subscribed to before you change their value. The
.value
property of a sub-state might also be out of sync if it is not subscribed to. When you change its value, it will issue a change to the state-tree regardless of whether or not it is subscribed, the change might have the wrong history if.value
is not in sync.
RxDeep requires the state-tree to be represented by plain JavaScript objects, i.e.
numbers, booleans, strings, Date
objects, undefined
, null
, or arrays / objects
of other plain JavaScript objects, without any circular references:
1linktype PlainJavaScriptObject = null | undefined | number | string | boolean | Date
2link | PlainJavaScriptObject[]
3link | {[key: string]: PlainJavaScriptObject}
This is essential since otherwise RxDeep is unable to perform post-tracing on changed objects.
You can subscribe to .downstream
property of a state to listen for changes occuring to it
(instead of just updated values):
1linkteam.sub('people').downstream.subscribe(console.log);
2linkteam.sub('people').sub(0).sub('name').value = 'Jackie';
3link
4link// Logs:
5link// > {
6link// > value: [ { id: 101, name: 'Jackie', id: 102, name: 'Jeremy' } ],
7link// > trace: {
8link// > subs: {
9link// > people: {
10link// > subs: {
11link// > 0: {
12link// > subs: {
13link// > name: { from: 'Jeremy', to: 'Jackie' }
14link// > }
15link// > }
16link// > }
17link// > }
18link// > }
19link// > }
20link// > }
A state is constructed with an initial value, a downstream (Observable<Change>
), and an upstream (Observer<Change>
).
Downstream is basically where changes to this state come from.
Upstream is where this state should report its changes.
1link// In this example, the value of `s` is updated with a debounce, so
2link// changes made in rapid succession are supressed.
3link
4linkconst echo = new Subject<Change<number>>();
5linkconst s = new State(3, echo.pipe(debounceTime(300)), echo);
6link
7links.subscribe(console.log);
8link
9links.value = 4; // --> gets supressed
10links.value = 10; // --> gets through
11linksetTimeout(() => s.value = 15, 310); // --> gets through
12linksetTimeout(() => s.value = 17, 615); // --> gets supressed
13linksetTimeout(() => s.value = 32, 710); // --> gets through
14link
15link// Logs:
16link// > 3
17link// > 10
18link// > 15
19link// > 32
1link// In this example, the value of `s` will remained capped at 10.
2link
3linkconst echo = new Subject<Change<number>>();
4linkconst s = new State(3,
5link echo.pipe(map(change => ({
6link ...change,
7link value: Math.min(change.value!!, 10),
8link }))),
9link echo
10link);
11link
12links.subscribe(console.log);
13link
14links.value = 4; // --> ok
15links.value = 12; // --> changes to 10
16links.value = 9; // --> ok
17links.value++; // --> ok
18links.value++; // --> caps
19link
20link// Logs:
21link// > 3
22link// > 4
23link// > 10
24link// > 9
25link// > 10
26link// > 10
When a new value is set on a state, it creates a Change
object and passes it up the
upstream. The state WILL NOT immedialtely emit said value. Instead, it trusts that if the value
corresponding to a particular change should be emitted, it will eventually come down the downstream.
Thats how we are able to modify the value of requested changes in above examples.
The upstream and downstream of sub-states is set by the parent state. When a change is issued to a sub-state, the parent state will pick up the change, add the corresponding sub-key to the change trace, and send it through its own upstream. When a change comes down its downstream, it will match it with sub-states using the sub-key specified in the change trace and down-propagate it accordingly, removing the head of the change trace in the process.
touch_app IMPORTANT
For performance reasons, a state will change its local
.value
when it is sending changes up the upstream, but will not emit them. This means if you want to completely reverse the effect of some particular change, you should emit a reverse change object on the downstream in response. Simply ignoring a change will cause the state's value to go out of sync with the change history and the emission history.
info NOTE
Note that if you want to create a state by providing specific upstream and downstream parameters, you need to use
State
class constructor.
When a change is issued to a non-leaf node, the change trace that is collected is up to that particular point of the state-tree, and not further down. As a result, a state does not know how to down propagate this change to its sub-states.
In such a case, the state will actually retrace the change based on given values, adding a complete trace to the change object, and then routing it accordingly. This operation only happens at the depth that the change was made, since further down the trace is complete down to leaf-states. Additionally, due to efficient object-tree diffing, this single operation does not slow down the propagation of change by any means. Checkout the post on performance to see how in details, but intuitively if the change is not properly traced, then all of the sub-tree would have to emit it in order to not be lossy (not emit when something is truly changed), which equals to a full sweep of the sub-tree, which could be used to diff the sub-tree instead.