Nearly Perfected Mystique

    TypeScript icon, indicating that this package has built-in type declarations

    0.10.1 • Public • Published


    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]


    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.


    yarn add subx
    import SubX from 'subx'

    Quickstart sample

    const person = SubX.create()
    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.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: '' })
    delete person.firstName


    GET events are triggered when we access a property

    const person = SubX.create({ firstName: '' })


    GET events are triggered when we use the in operator

    const person = SubX.create({ firstName: '' })
    console.log('firstName' in person)


    KEYS events are triggered when we use Object.keys(...)

    const person = SubX.create({ firstName: '' })

    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.


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


    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)


    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.


    npm i subx

    DownloadsWeekly Downloads






    Unpacked Size

    253 kB

    Total Files


    Last publish


    • tylerlong