Roox
Immutable, object-oriented state manager for React.
This Document is outdated.
Table of contents
Introduction
Roox is a simple state manager for React, and combines immutable state with object-oriented programming.
Roox is implemented in typescript, and all code in this document is typescript code. But you can also use es6 instead.
The core concept of Roox is RooxState
-
RooxState is immutable.
-
RooxState can define reducer, which is the only way to update state.
Roox is easy to use, let's look at a simple example ,and then introduce Roox by explaining this example.
Example: Counter
code structure
-
src
- components
- Counter.tsx
- states
- CounterState.ts
- index.tsx
- components
CounterState.ts
; ;
Counter.tsx
;;
index.tsx
;;;;; ; store.subscribe render;
RooxState
At first, we define a class named CounterState, which extends RooxState. RooxState is the core of Roox. In the following, when we referer RooxState, we mean all objects of RooxState and its derived class.
RooxState is immutable, so you can't modify its property. In fact, when a RooxState is added to the store, Roox will call Object.freeze to ensure its immutability.
RooxState has two impotant features:
- define reducer.
- call
track
.
reducer
The only way to update RooxState is to define and call a reducer. when you call a reducer, the reducer will produce a new RooxState, which replace the old RooxState.
define reducer
defining a reducer is just like defining a normal method, but you must annotated the reducer with @redcuer (@reducer is a syntax of es6 and typescript called decorator).
In the Counter example, CounterState has 3 reducers: inc, dec and add, which modify the value of counter: number
.
A reducer can return one of the following types of value
-
a new RooxState.
-
a diff. Roox will patch the diff to
this
, and produce a new RooxState.
A diff is a plain object, and its key must be a property name of this
.Roox will ensure this at runtime, using code like this: if (!(key in this)) throw xxx
. If you use typescript, it's recommended to annotated the return type with Partial<xxxState>
, then typescript can ensure the diff is legal at compile time.
In the Counter example, we return a diff of shape { counter: xxx }
, which means the only property we mean to update is counter
. the inc
reducer is equal to the following code which just return a new CounterState:
inc
you must initialize RooxState properties before call its reducer. In typescript, declaring a property without initialing it won't generate any code after compile, which will cause Roox throw error when check the diff that a reducer returned.
If you want to add property danamicly, you can use RooxMap
.
Definition of a reducer should be pure.
call reducer
Calling a reducer is just like calling a normal method, but the @reducer
decorator does add additional semantic. Although reducer definition is pure, calling a reducer does have side effect : update the state tree, and @reducer
does the magic.
After you call a reducer, it produce a new RooxState, either just use the return value or patch the diff it returned to the old RooxState. Roox create a new immutable state tree using this new RooxState with all ancestor states copyed and updated, and the old state tree is detached from the store.
The following is the most impotant thing to remember when you call a reducer.
reducer can only be called on a RooxState in the store.
It means that you can only call reducers once on a specific RooxState. The following code is wrong:
counterState.inc; // After this call, counterState1 has been removed from storecounterState.dec; // Wrong.
Ancestor RooxStates are also updated when you call a reducer.
......;counterState.inc;// Wrong, appState has been removed from store because we call reducer on counterState.appState.foo;
Calling a reducer always returns a new RooxState. When the reducer definition returns a diff, the actual return value is the new state patched from the diff. @reducer
does the magic. so you can write code like this:
// Annotate counterState with type Partial<CounterState> because reducer annotated return type of Partial<CounterState>.// In fact, it's always a full CounterState.; counterState = counterState.inc;counterState = counterState.dec;
We can also write like this:
counterState.inc.dec;
Don't call reducer on RooxState in async code. Because you can't ensure the RooxState is in store when aysnc code runs.
setTimeout, 1000;
You can use track
in async code.
track
Let's recap the incAsync
method of CounterState
incAsync
Here we use another feature of RooxState: track.
Calling track
on a RooxState will returns a proxy object. Sometimes we also call this proxy object track for simplicity.
This proxy object can do anything the RooxState can do, such as calling a reducer. calling a reducer won't invalidate the proxy object, so we can write like this:
;track.inc;track.dec;
the proxy object track
returned contains two information
- store:a tree of RooxState.
- path: a path in the tree.
For example, if we have a state tree like the following:
Then the path of counterState
is /innerState/counterState
.
Everytime we do something on a the proxy object, Roox will get the fresh RooxState by following the path in the store. The following code
;track.inc;track.dec
is equal to
store.getState.innerState.counterState.inc;store.getState.innerState.counterState.dec;
How Roox works
The key idea of Roox is simple: state shapes a tree, and a path in the tree can act as the identify of an object. We think different RooxStates in the same path as different values of a object. This way of thinking makes it possible to combine immutable state with object-oriented programming.
How Roox works:
- RooxStates shape into a immutable tree.
- A RooxState can only appear once in the tree, so there is a one-one mapping betwen a RooxState and a tree node in the tree.
- Properties of the RooxState act as the children of the tree node.
- Calling a reducer will produce a new RooxState, which is placed into the tree node that the old RooxState was mapping to, and a new immutable state tree is created.
API
Store<T extends RooxState>
A Store
holds a immutable state tree, and you can listen to state changes using subscribe
.
constructorinitialState: T
initialState
can't be null.
getState: T;
Get the root of the RooxState tree.
subscribelistener:any:void;
Listen to state changes.
RooxState
trackoption?: TrackOption<this>: Track<this>
Get a proxy object pointing to the tree node this
mapping to.
inStore: boolean
Check if this is in the store.
RooxArray<T>
A subclass of RooxState which wraps JavaScript Array.
constructorpublic data: T
data: T
Get the wrapped array object.
callReducercallback:T
Update data
with the return value of callback
. the argument passed to callback
is the old data
.
setindex: number, value: T
Update a member of array.
push...items: T
Wrap Array.push.
pop
Wrap Array.pop.
shift
Wrap Array.shift.
unshift...items: T
Wrap Array.unshift.
splicestart: number, deleteCount?: number, ...items: T
WrapArray.splice.
getindex: number
Get a member of array.
length: number
wrap Array.length.
RooxMap<K extends string | number, V>
A subclass of RooxState which wraps JavaScript Map. The type of key can only be string or number.
constructorpublic data: Map<K, V>
data: Map<K, V>
Get the wrapped Map.
callReducercallback:Map<K, V>
Update data
with the return value of callback
. the argument passed to callback
is the old data
.
setkey: K, value: V
Add or update a member of map.
delete
Delete a member of map.
clear
Clear map.
getkey: K
Get a member of map.
RooxWrapper<T>
A subclass of RooxState which wraps non-roox data. T
must not contains RooxState.
The following is wrong:
// Wrong, call reducer on CounterState will not work.RooxWrapperImmutable.List<CounterState>
Because CounterState can't placed into RooxWrapper.
constructorpublic data: T
data: T
The wrapped data.
callReducercallback:T
Update data
with the return value of callback
. the argument passed to callback
is the old data
.
Track<T>
A proxy object pointing to a specific path of the RooxState tree.
You can get a Track
object by calling track
method on a RooxState.
$getTrackedState: T;
Get the RooxState this proxy object pointing to.
$isCheckFailed: boolean;
Check if check
failed.
$notNull: boolean;
Check if the path has a valid RooxState.
$isNull: boolean;
Equal to !$isNull
.
Compare with Redux
Roox is inspired by Redux, and motivated by the inconvinience of Redux.
Same with Redux
- Immutable state
- Single store
- Pure reducer
Difference
- Object-oriented style programming. Just define and call reducer, no needs for action.
- No combineReducer.
- No connect.Roox is almost othonomal with React.
- No mapStateToProps / mapDistatchToProps, Roox encourage you to pass state as React props.
- Forced immutable State.
LICENSE
MIT