
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
Immutable, object-oriented state manager for React.
This Document is outdated.
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.
code structure
src
CounterState.ts
import { RooxState, reducer } from 'roox';
type P = Partial<CounterState>;
export class CounterState extends RooxState {
counter = 0;
@reducer
inc(): P {
return { counter: this.counter + 1 }
}
@reducer
dec(): P {
return { counter: this.counter - 1 }
}
@reducer
add(n: number): P {
return { counter: this.counter + n }
}
incIfOdd(): P {
return this.isOdd() ? this.inc() : this;
}
incAsync() {
let track = this.track();
setTimeout(() => track.inc(), 1000);
}
isOdd() {
return this.counter % 2 === 1;
}
}
Counter.tsx
import * as React from 'react';
import { CounterState } from '../states/CounterState';
export function Counter(props: {counterState: CounterState}) {
const { counterState } = props;
return <p>
Clicked: {counterState.counter} times{' '}
<button onClick={() => counterState.inc()}>+</button>{' '}
<button onClick={() => counterState.dec()}>-</button>{' '}
<button onClick={() => counterState.incIfOdd()}>Increment if odd</button>{' '}
<button onClick={() => counterState.add(2)}>add 2</button>{' '}
<button onClick={() =>counterState.incAsync()}>Increment async</button>{' '}
</p>
}
index.tsx
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Store } from 'roox';
import { Counter } from './components/Counter';
import { CounterState } from './states/CounterState';
const store = new Store(new CounterState());
function render() {
ReactDOM.render(
<Counter counterState={store.getState()} />,
document.getElementById('root')
)
}
store.subscribe(() => {
render();
})
render();
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:
track.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.
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:
@reducer inc() {
let newCounterState = new CounterState();
newCounterState.counter = this.counter + 1;
return newCounterState;
}
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.
class CounterState extends RooxState {
counter; // wrong: counter must be initialized, like counter = 0;
// ......
}
If you want to add property danamicly, you can use RooxMap.
Definition of a reducer should be pure.
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 store
counterState.dec(); // Wrong.
Ancestor RooxStates are also updated when you call a reducer.
class AppState {
@reducer foo() { ... }
counterState = new CounterState();
}
......
let counterState = appState.counterState;
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.
let Partial<CounterState> counterState = store.getState();
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(() => {
// Wrong, counterState may have been removed from store when timer fired.
counterState.inc();
}, 1000);
You can use track in async code.
Let's recap the incAsync method of CounterState
incAsync() {
let track = this.track();
setTimeout(() => track.inc(), 1000);
}
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:
let track: Track<CounterState> = counterState.track();
track().inc();
track().dec();
the proxy object track returned contains two information
For example, if we have a state tree like the following:
class AppState {
innerState = new InnerState();
}
class InnerState {
counterState = new CounterState();
}
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
let track = store.getState().innerState.counterState.track();
track.inc();
track.dec()
is equal to
store.getState().innerState.counterState.inc();
store.getState().innerState.counterState.dec();
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:
A Store holds a immutable state tree, and you can listen to state changes using subscribe.
constructor(initialState: T)
initialState can't be null.
getState(): T;
Get the root of the RooxState tree.
subscribe(listener: () => any): () => void;
Listen to state changes.
@reducer
track(option?: 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.
A subclass of RooxState which wraps JavaScript Array.
constructor(public data: T[])
data: T[]
Get the wrapped array object.
@reducer
callReducer(callback: (data: T[]) => T[])
Update data with the return value of callback. the argument passed to callback is the old data.
@reducer
set(index: number, value: T)
Update a member of array.
@reducer
push(...items: T[])
Wrap Array.push.
@reducer
pop()
Wrap Array.pop.
@reducer
shift()
Wrap Array.shift.
@reducer
unshift(...items: T[])
Wrap Array.unshift.
@reducer
splice(start: number, deleteCount?: number, ...items: T[])
WrapArray.splice.
get(index: number)
Get a member of array.
length: number
wrap Array.length.
A subclass of RooxState which wraps JavaScript Map. The type of key can only be string or number.
constructor(public data: Map<K, V>)
data: Map<K, V>
Get the wrapped Map.
@reducer
callReducer(callback: (data: Map<K, V>) => Map<K, V>)
Update data with the return value of callback. the argument passed to callback is the old data.
@reducer
set(key: K, value: V)
Add or update a member of map.
@reducer
delete(key: K)
Delete a member of map.
@reducer
clear()
Clear map.
get(key: K)
Get a member of map.
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.
RooxWrapper(Immutable.List<CounterState>)
Because CounterState can't placed into RooxWrapper.
constructor(public data: T)
data: T
The wrapped data.
@reducer
callReducer(callback: (data: T) => T)
Update data with the return value of callback. the argument passed to callback is the old data.
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.
Roox is inspired by Redux, and motivated by the inconvinience of Redux.
Same with Redux
Difference
MIT
FAQs
Immutable, object-oriented state manager for React
We found that roox demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.