Skip to content

Latest commit

 

History

History
283 lines (179 loc) · 8.76 KB

README.md

File metadata and controls

283 lines (179 loc) · 8.76 KB

Discontinued

This project has been replaced by manate.

SubX

Build Status npm version

SubX is next generation state container. It could replace Redux and MobX in 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.

It is easy to convert a SubX object to a plain object: const plainObj = subxObj.toObject().

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 = 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.

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:

  1. When we invoke autoRun, the second argument f is invoked immediately.
  2. Then the the first argument subx is monitored.
  3. Whenever subx changes which might affect the result of f, f is invoked again.
  4. The invocation of f is further controlled by ...operators.
  5. The result of f() are directed to the returned stream$
  6. We can stream$.subscribe(...) to consume the results of f()
  7. We can stream$.complete() to stop the whole monitor & autoRun process described above.

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 of f()
    • stream$ is a stream (RxJS Subject)

How does runAndMonitor work:

  1. When we invoke runAndMonitor, the second argument f is invoked immediately.
  2. Result of f() is saved into result
  3. Then the the first argument subx is monitored.
  4. Changes to subx which might affect the result of next invocation of f are redirected to stream$
  5. { result, stream$ } is returned
  6. We can stream$.pipe(...operators).subscribe(...) to react to the stream events (possibly invoke f again)

Sample code using runAndMonitor

Recursive

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.

Convert recursive to non-recursive

let p = SubX.create({ a: {}, b: {} })
p = SubX.create(p.toObject(), false)

Convert non-recursive to recursive

let p = SubX.create({ a: {}, b: {} }, false)
p = SubX.create(p)

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.