@electerm/subx

SubX, Next generation state container

Usage no npm install needed!

<script type="module">
  import electermSubx from 'https://cdn.skypack.dev/@electerm/subx';
</script>

README

SubX

Build Status npm version

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:

  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.

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

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.