This project has been replaced by manate.
SubX is next generation state container. It could replace Redux and MobX in React apps.
Subject X, Reactive Subject. Pronunciation: [Sub X]
If you want to use SubX together with React, please check react-subx.
- 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.
yarn add subx
import SubX from 'subx'
const person = SubX.create()
person.$.subscribe(console.log)
person.firstName = 'Tyler'
person.lastName = 'Long'
{ 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.
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.
It is easy to convert a SubX object to a plain object: const plainObj = subxObj.toObject()
.
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$
.
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
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
events are triggered when we access a property
const person = SubX.create({ firstName: '' })
person.get$.subscribe(console.log)
console.log(person.firstName)
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
events are triggered when we use Object.keys(...)
const person = SubX.create({ firstName: '' })
person.keys$.subscribe(console.log)
console.log(Object.keys(person))
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.
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 = SubX.model({
firstName: 'San',
lastName: 'Zhang',
get fullName () {
return `${this.firstName} ${this.lastName}`
}
})
const person = Person.create()
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.
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)
- 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.
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)
- 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)
- test/react_fake.spec.ts
- test/monitor_delete.spec.ts
- test/react.spec.ts
- test/runAndMonitor_path.spec.ts
By default, a SubX Object is recursive. Which means, all of its property objects are also SubX objects. For example:
const p = SubX.create({ a: {}, b: {} })
p
is a SubX object, so are p.a
and p.b
.
You can disable the recursive behavior:
const p1 = SubX.create({ a: {}, b: {} }, false)
const P = SubX.model({ a: {}, b: {} }, false)
const p2 = P.create()
p1
and p2
are SubX objects while none of p1.a
, p1.b
, p2.a
, p2.b
are SubX objects.
let p = SubX.create({ a: {}, b: {} })
p = SubX.create(p.toObject(), false)
let p = SubX.create({ a: {}, b: {} }, false)
p = SubX.create(p)
If we create circular data structure with SubX, the behavior is undefined. Please don't do that.
Please read the wiki. We have a couple of useful pages there.
Our test cases have lots of interesting ideas too.