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.firstNameGET 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_BEGINis triggered when a computed property starts to compute.COMPUTE_FINISHis triggered when a computed property finishes computing.STALEis 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
subxis a SubX object - Second arugment
fis an action/function - Remaining arguments
...operatorsare RxJS operators - Return type
stream$is a stream (RxJS Subject)
- When we invoke
autoRun, the second argumentfis invoked immediately. - Then the the first argument
subxis monitored. - Whenever
subxchanges which might affect the result off,fis invoked again. - The invocation of
fis 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
subxis a SubX object - Second arugment
fis an action/function - Return type is an object which containers two properties:
resultis the result off()stream$is a stream (RxJS Subject)
- When we invoke
runAndMonitor, the second argumentfis invoked immediately. - Result of
f()is saved intoresult - Then the the first argument
subxis monitored. - Changes to
subxwhich might affect the result of next invocation offare redirected tostream$ { result, stream$ }is returned- We can
stream$.pipe(...operators).subscribe(...)to react to the stream events (possibly invokefagain)
- 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.