Fast and precise reactive state management for JavaScript / TypeScript, in a flexible and unopinionated manner. Make changes to any part of your state tree, track changes, subscribe to specific node/sub-tree, track changes by entity keys, verify changes, etc.
1linknpm i rxdeep
Create a state:
1linkimport { state } from 'rxdeep';
2linkconst s = state([ { name: 'John' }, { name: 'Jack' }, { name: 'Jill' } ]);
Listen to a sub-state:
1links.sub(1).sub('name').subscribe(console.log); // --> subscribes to property `name` of object at index 1 of the array
Modify root state:
1links.value = [ { name: 'Julia' }, ...s.value ]; // --> logs `John`, since `John` is index 1 now
... or mid-level states:
1links.sub(1).value = { name: 'Josef' }; // --> logs `Josef`
... or leaf-states on the same address:
1links.sub(1).sub('name').value = 'Jafet'; // --> logs `Jafet`
Verify changes to the state:
1linkimport { state, verified } from 'rxdeep';
2link
3linkconst v = verified(state(12), change => change.from < change.to); // --> only increasing numbers
4link
5linkv.subscribe(console.log);
6link
7linkv.value = 10; // --> logs 12
8linkv.value = 14; // --> logs 14
9linkv.value = 9; // --> logs 14
10linkv.value = 13; // --> logs 14
11linkv.value = 15; // --> logs 15
A state is an Observer
:
1linkimport { interval } from 'rxjs';
2linkimport { map } from 'rxjs/operators';
3link
4linkinterval(1000)
5link.pipe(map(i => ({ name: `Jarvis #${i}`})))
6link.subscribe(s.sub(1)); // --> logs `Jarvis #0`, `Jarvis #1`, `Jarvis #2`, ...
A state is an Observable
:
1linkimport { debounceTime } from 'rxjs/operators';
2link
3links.sub(1).pipe(debounceTime(1000)).subscribe(console.log); // --> debounces changes for 1 second
Track keys instead of indexes:
1linkimport { state, keyed } from 'rxdeep';
2link
3linkconst k = keyed(state(
4link [{ id: 101, name: 'Jill' }, { id: 102, name: 'Jack' }]
5link ), p => p.id
6link);
7link
8linkk.key(101).sub('name').subscribe(console.log); // --> logs `Jill`
9link
10linkk.value = [k.value[1], k.value[0]]; // --> no log
11linkk.sub(1).sub('name').value = 'John'; // --> logs `John`
Track index of a specific key:
1linkk.index(101).subscribe(console.log); // --> logs 0, 1
RxDeep is not by any means limited to frontend use. However most backend designs are state-less or with dedicated services managing states, which means its most common use-case is in the frontend.
RxDeep is also completely framework agnostic, with a precise emission system that allows surgical updates even if you are using pure JavaScript without the need for a Virtual DOM, passive change detection, etc.
These precise emissions also should reduce the change detection / DOM reconcilliation load on most popular frameworks, as re-renders can be requested only when something has truly changed, and specifically on the DOM sub-tree that have really changed.
You can use RxJS-hooks for fetching and rendering states in React components. This should also result in better performance as it should reduce number of redundant tree diffs conducted by React.
1linkimport { useObservable } from 'rxjs-hooks';
2link
3linkconst s = state(...);
4link
5linkfunction MyComponent(...) {
6link const value = useObservable(() => s.sub('something')) || {};
7link return <div>{value.property}</div>
8link}
You can use Angular's async pipe to render states or sub-states:
1link<div>{{s | async}}</div>
2link<div>{{s.sub('property') | async}}</div>
You can also utilize Angular's [(ngModel)]
syntax to bind states to inputs:
1link<input [(ngModel)]="s.sub('property').value" type="text" />
You can use vue-rx to render states in your Vue apps, which should (I don't know if it would) yield similar performance improvements:
1linkimport VueRx from 'vue-rx';
2linkVue.use(VueRx);
3link
4linknew Vue({
5link el: '#app',
6link subscriptions: {
7link s,
8link name: s.sub('name')
9link }
10link});
1link<div>{{ s }}</div>
2link<div>{{ name }}</div>
You can use v-model
syntax to directly bind inputs and states:
1linknew Vue({
2link ...
3link subscriptions: { ... },
4link data: { state }
5link})
1link<input type="text" v-model="s.sub('name').value"/>
As mentioned above, RxDeep provides precision change emission, which means you can directly listen to the state changes and surgically update DOM tree accordingly:
1links.sub('name').subscribe(name => nameElement.textContent = name);
Specifically, you can utilize KeyedState
(keyed()
) and its .changes()
method to receive detailed
array changes that enable you to precisely modify dynamic DOM trees based on collections
and arrays.
Here are the design goals/features of RxDeep, setting aside from other reactive state management libraries:
RxDeep is extremely fast and light-weight in terms of memory consumption and computation, utilizing pure (multicasted) mappings on the root of the state-tree for reading/writing on the whole tree.
Note that user-provided functions are utilized during particular operations, which might result in some loss of performance if said functions aren't fast enough.
RxDeep enables subscribing to a particular sub-state of the state tree. These sub-states only emit values when the value of the sub-state has changed, or when there is a change issued directly to them, a state with the same address, or one of their descendants.
This means you could subscribe heavy-weight operations (such as DOM re-rendering) on sub-states.
RxDeep, unlike libraries such as Redux, doesn't require your changes to be funneled through specific channels. You can freely issue changes to any part of the state-tree, so for example you can only expose relevant parts of the state-tree to modules/components.
The only limitations (similar to Redux) are that you need to
keep state as plain JavaScript objects (number | string | boolean | undefined | Date
, or arrays and plain objects
of these values), and respect object immutability.
Basically do not change an object without changing its reference.
DON'T:
1link/*~*/s~.~value~~~~~.~push~~~~(~x~)~/*~*/; // --> WRONG!
2link/*~*/s~.~value~~~~~.~x ~~=~ y~~/*~*/; // --> WRONG!
DO:
1links.value = s.value.concat(x); // --> CORRECT!
2links.sub('x').value = y; // --> CORRECT!
3links.value = { ...s.value, x: y } // --> CORRECT!
State tree is kept in sync by tracking changes (via Change
objects). This simply means you can track changes
directly, record them, replay them, etc.
1links.downstream.subscribe(console.log); // --> Log changes
2links.sub(1).sub('name').value = 'Dude';
3link
4link// This object will be logged:
5link{
6link value: [{...}, { name: 'Dude', ... }, ...],
7link trace: {
8link subs: {
9link 1: {
10link subs: {
11link name: { from: ..., to: 'Dude' }
12link }
13link }
14link }
15link }
16link}
Furthermore, keyed states provide detailed array changes, i.e. additions/deletions on particular indexes, or items being moved from one index to another.
1linkconst k = keyed(state(
2link [{ id: 101, name: 'Jack' }, { id: 102, name: 'Jill' }]
3link ), p => p.id
4link);
5link
6linkk.changes().subscribe(console.log); // --> Log changes
7link
8linkk.value = [
9link { id: 102, name: 'Jill' },
10link { id: 101, name: 'Jack' },
11link { id: 103, name: 'Jafet' }
12link];
13link
14link// This object will be logged:
15link{
16link additions: [{
17link index: 2,
18link item: { id: 103, name: 'Jafet' }
19link }],
20link deletions:[],
21link moves:[
22link { oldIndex: 0, newIndex:1, item: { id: 101, name: 'Jack'} },
23link { oldIndex: 1, newIndex:0, item: { id: 102, name: 'Jill'} }
24link ]
25link}
You can verify changes occuring on the state-tree (or on a particular sub-tree). RxDeep will utilize the change history to revert unverified changes on affected sub-states:
1linkconst v = verified(
2link state([{ val: 21 }, { val: 22 }, { val: 23 }]),
3link change => change.value.reduce((t, i) => t + i.val) % 2 === 0
4link);
5link
6linkv.sub(0).sub('val').value = 22; // --> change denied, local changes automatically reverted
7linkv.sub(0).sub('val').value = 23; // --> change accepted and routed through the state-tree
An RxDeep state is an RxJS Observable
and an Observer
, providing great interoperability with lots of existing tools.
Additionally, each state basically relies on a downstream observable and an upstream observer for keeping track of changes and keeping data in sync. By providing custom downstream / upstreams, you can greatly extend RxDeep for use in any particular use case (for example you can easily distribute state-trees across a network).
RxDeep has a bundle size of ~8Kb
, which includes its only dependency RxJS (tree-shaken).
Since RxJS is already included in lots of frontend bundles, contribution of RxDeep to your
bundle size will most probably be under 2Kb
(which is the raw library without dependencies).
This small size is due to extremely thin API surface of the library, focusing only on providing deep state management and minor utilities for that. This in turn makes RxDeep pretty easy to learn.
RxDeep is written in TypeScript with detailed type annotations, which should greatly improve development experience even if you use it in JavaScript (error highlighting, autocompletes, etc).
Reactive state-management using RxJS. Manage complex state-trees in a fast, precise and flexible manner. Monitor changes at any part of the state-tree, issue changes at any part of the state tree, verify changes, record them, replay them, distribute your state tree across network, etc.