@electerm/subx v0.8.3
SubX
SubX is next generation state container. It could replace Redux and MobX in our React apps.
Subject X, Reactive Subject. Pronunciation: Sub X
react-subx
If you want to use SubX together with React, please check react-subx.
Features (compared to Redux or MobX)
- Developer-friendly: fewer lines of code to write, fewer new concepts to learn & master.
- Intuitive, just follow common sense. No annotation or weird configuration / syntax.
- Performant, it helps us to minimize backend computation and frontend rendering.
- Based on RxJS, we can use ALL the RxJS operators.
- Schemaless, we don't need to specify all our data fields at the beginning. We can add them gradually and dynamically.
- Small. 400 lines of code. (Unbelievable, huh?) We've written 5000+ lines of testing code to cover the tiny core.
Installation
yarn add subx
import SubX from 'subx'
Quickstart sample
const person = SubX.create()
person.$.subscribe(console.log)
person.firstName = 'Tyler'
person.lastName = 'Long'
Console output
{ type: 'SET', path: ['firstName'], id: 'uuid-1' }
{ type: 'SET', path: ['lastName'], id: 'uuid-2' }
In the sample code above, person
is a SubX object. person.$
is a stream of events about changes to person
's properties.
If you know RxJS
, I would like to mention that person.$
is an Observable.
What is a SubX Object / Reactive Subject?
Subject is the similar concept as the subject in observer pattern.
A reactive subject is a special JavaScript object which allows us to subscribe to its events. If you are a React + Redux developer, events is similar to actions. If you are a Vue.js + Vuex developer, events is similar to mutations.
In content below, we call a reactive subject a SubX object.
Types of events
Currently there are 5 basic events: SET
, DELETE
, GET
, HAS
& KEYS
.
The corresponding event streams are set$
, delete$
, get$
, has$
& keys$
There are 3 advanced events: COMPUTE_BEGIN
, COMPUTE_FINISH
& STALE
.
The corresponding event streams are compute_begin$
, compute_finish$
& stale$
.
set$
& $
Most of the event mentioned in this page are SET
events. SET
means a property has been assigned to. Such as person.firstName = 'John'
.
const person = SubX.create({ firstName: 'Tyler' })
person.set$.subscribe(console.log)
person.firstName = 'Peter'
$
is a synonym of set$
. We provide it as sugar since set$
is the mostly used event.
delete$
DELETE
events are triggered as well. We already see one of such event above in "Array events" section. Here is one more sample:
const person = SubX.create({ firstName: '' })
person.delete$.subscribe(console.log)
delete person.firstName
get$
GET
events are triggered when we access a property
const person = SubX.create({ firstName: '' })
person.get$.subscribe(console.log)
console.log(person.firstName)
has$
GET
events are triggered when we use the in
operator
const person = SubX.create({ firstName: '' })
person.has$.subscribe(console.log)
console.log('firstName' in person)
keys$
KEYS
events are triggered when we use Object.keys(...)
const person = SubX.create({ firstName: '' })
person.keys$.subscribe(console.log)
console.log(Object.keys(person))
compute_begin$
, compute_end$
& state$
These 3 events are advanced. Most likely we don't need to know them. They are for computed properties(which is covered below).
COMPUTE_BEGIN
is triggered when a computed property starts to compute.COMPUTE_FINISH
is triggered when a computed property finishes computing.STALE
is triggered when the computed property becomes "stale", which means a re-compute is necessary.
Getters / Computed properties
We use "convention over configuration" here: getter functions are computed properties. If we don't need it to be computed property, just don't make it a getter function.
So in SubX, "computed properties" and "getters" are synonyms. We use them interchangeably.
const Person = new SubX({
firstName: 'San',
lastName: 'Zhang',
get fullName () {
return `${this.firstName} ${this.lastName}`
}
})
const person = new Person()
expect(person.fullName).toBe('San Zhang')
What is the different between computed property and a normal function? Computed property caches its results, it won't re-compute until necessary.
So in the example above, we can call person.fullName
multiple times but it will only compute once. It won't re-compute until we change either firstName
or lastName
and invoke person.fullName
again.
I would recommend using as many getters as we can if our data don't change much. Because they can cache data to improve performance dramatically.
Computed properties / getters are supposed to be "pure". We should not update data in them. If we want to update data, define a normal function instead of a getter function.
autoRun
The signature of autoRun
is
// autoRun :: (subx, f, ...operators) -> stream$
Method signature explained:
- First agument
subx
is a SubX object - Second arugment
f
is an action/function - Remaining arguments
...operators
are RxJS operators - Return type
stream$
is a stream (RxJS Subject)
How does autoRun
work:
- When we invoke
autoRun
, the second argumentf
is invoked immediately. - Then the the first argument
subx
is monitored. - Whenever
subx
changes which might affect the result off
,f
is invoked again. - The invocation of
f
is further controlled by...operators
. - The result of
f()
are directed to the returnedstream$
- We can
stream$.subscribe(...)
to consume the results off()
- We can
stream$.complete()
to stop the whole monitor & autoRun process described above.
Sample code using autoRun
runAndMonitor
runAndMonitor
is low level API which powers autoRun
. If for some reason autoRun
is not flexible enough to meet your requirements, you can give runAndMonitor
a try.
The signature of runAndMonitor
is:
// runAndMonitor :: subx, f -> { result, stream$ }
Method signature explained:
- First agument
subx
is a SubX object - Second arugment
f
is an action/function - Return type is an object which containers two properties:
result
is the result off()
stream$
is a stream (RxJS Subject)
How does runAndMonitor
work:
- When we invoke
runAndMonitor
, the second argumentf
is invoked immediately. - Result of
f()
is saved intoresult
- Then the the first argument
subx
is monitored. - Changes to
subx
which might affect the result of next invocation off
are redirected tostream$
{ result, stream$ }
is returned- We can
stream$.pipe(...operators).subscribe(...)
to react to the stream events (possibly invokef
again)
Sample code using runAndMonitor
Pitfalls
Circular data
If we create circular data structure with SubX, the behavior is undefined. Please don't do that.
More info
Please read the wiki. We have a couple of useful pages there.
Our test cases have lots of interesting ideas too.
4 years ago