Last week, I completely give up on Redux after a year and go back to drawing board to

solve the problem of state management. Manage numerous and ever changing state is extremely hard, but it doesn’t mean we should make it ridiculous.

Don’t get me wrong, I’m big fan of Command Query Responsibility Segregation pattern which Redux inspired by. Redux’s CQRS is awkward with “Object.assign” reducers (Flux is more comprehensive).

I believe that mutable javascript object with all its faults; is a natural and efficient way for manage complex state so I spend last weekend to draft and implement GState — my own view about state management.

How it work?

Simple React clock:

GState is very simple:

Create a state:

var state = new GState();

Put an value into state:

state.set({ a: “a”});

Query state: (syntax borrow from my own project NextQL)

state.get({ a: 1});

Or watch state changes (same with state.get but reactive)

state.watch({ a: 1}, result => result);

So it looks like an in-memory database?

Kind of with more few tricks.

Nested object/nested query:

Notice: gstate watch based on query result not state’s value. So it’s able track changes from not-existed object. Whenever new objects input, watches triggered if their results changed.

Reference and Circular reference

Yes, GState able store and reactive against JS reference and circular reference too. Check out GState parent/child demo

const parent = { name: “Giap” }; const child = { name: “Vinh”}; parent.child = child; child.parent = parent; store.set({ parent, child }); function update_parent_name(value) { store.set({ parent: { name: value } }); } function update_child_name(value) { store.set({ child: { name: value }}); }

Delete object (NOT just reference)

state.delete(path)

It is hard to remove an object in JS because you must delete all it’s references. The API should completely remove an object from state no matter it could be referenced (or circular referenced) in anywhere.

Notice: state.delete(“a.item1”) also remove reference at “b.item1”.

Sub state

state.path(key_or_keys)

The API create a sub state, any set/get/watch/delete would apply this tree only. Of cause, you could set/get/watch/delete sub state from parent state too.

const ctx = state.path(“a.b.c”); ctx.set({n: “n”}); ctx.get({n: 1}); // same with state.get({a:{b:{c:{n:1}}}});

Query Syntax

One I’m impressed from Relay is declarative data pattern. It should be best way to display application state. While Relay limit the pattern for data fetching, gstate use it for every kind of state.

Declarative data pattern

{

a: 1, // if a is primitive return its value

b: 1, // if b is object return all it's primitive properties

c: { // nested object should explicit declarce

d: 1

}

e: {

_: { // reserved for map operator: e.map(item => { f: item.f })

f: 1

}

} }

Gstate have an powerful onMapCallback which able customize map operation. For example, use awesome sift package to turn gstate query into mongodb-liked.

const sift = require("sift");

const state = new GState({

onMapCallback(op, nodes) {

if (op.query) { return sift(op.query, nodes); }

}

});

state.set({

group: {

person1: { name: "a1", age: 20 },

person2: { name: "a2", age: 30 },

person3: { name: "a3", age: 45 }

}

}); const res = state.get({ group: {

_: { name: 1, age: 1 },

query: { age: { $gt: 20 } } }

}); expect(res).toEqual({

group: [{ age: 30, name: "a2" }, { age: 45, name: "a3" }]

});

Concepts

CQRS stands for Command Query Responsibility Segregation. It’s a pattern that I first heard described by Greg Young. At its heart is the notion that you can use a different model to update information than the model you use to read information — Martin Fowler 14 July 2011

Normally, what we write is what we read. We store a person object into state management, we suppose able to read exact the object out. But when you have too many state, you find out: if your state easy to write, it would hard to read and vice versa.

For easy to write, underlying data model needs to be organized in a way that makes updates easier. While a read action needs to return data in the format the user wants to view them. CQRS solve the problem by to have two different models, one optimize for read and one for write.

GState’s CQRS

Query: state.get, state.watch use a denormalized tree-liked model to read information. It borrows idea from Relay/GraphQL (and syntax from my NextQL). The model is match with what UI look like.

Command: state.set, state.delete use natural JS object model which is normalized and graph-liked. The model is natural JS objects which is easy for mutation.

Isolated state

To solve the problem of change detection, gstate use isolated state. You cannot read or write state directly. Whenever you set a value, it make a deep-clone and merge with internal state. Whenever you query, it make a deep-clone internal state to create a result. Those processes make sure that gstate not only able track changes, it also know what you want to read.

Bonus

Gstate could work on server side too! Check out chat sample . It work as in-memory database which able notify changes.

var state = new GState(); var counter = Date.now(); io.on("connection", function(socket) {

console.log("client connect");

const watcher = state.watch({

messages: { _: 1 }

},

result => {

console.log(result);

if (result && Array.isArray(result.messages)) {



socket.emit("messages", result.messages.slice(-10));

}

}); socket.on("disconnect", function() {

console.log("client disconnect");

watcher();

}); socket.on("new message", function(msg) {

counter++;

const id = new Date().getTime() + counter % 1e9;

state.set({

messages: {

[id]: msg

}});

});

});

Finals

The project not ready for production yet, but I would like to hear comments from everybody.