head-of-state v1.0.2
head-of-state
Simple tool for managing the state of an application.
What does "managing the state of an application" mean?
From the React.js docs:
To build your app correctly, you first need to think of the minimal set
of mutable state that your app needs. The key here is DRY:
Don't Repeat Yourself.
Figure out the absolute minimal representation of the state your
application needs and compute everything else you need on-demand.
Extracting this idea into a more flux-y mindset, instead of simply having states local to components themselves, you may also have a global application-level state, which then triggers changes to the components as immutable properties (props).
Put more simply and to the point, the state of your application is a plain javascript object that can ideally describe your application at any point in time. This can be particularly useful if you wish to save your application state, replay states, go back to previous states, load states from a database or cookie, etc.
Installation
Can be used in a Node.js environment, or directly in the browser.
Node.js
npm install head-of-state
Browser
<script src="head-of-state.min.js"></script>
Usage
1) If in node.js, require
the state manager class:
var HeadOfState = require('head-of-state');
Else, include the minified version in a script tag in your browser and HeadOfState
will be globally available.
<script src="head-of-state.min.js"></script>
2) Create an instance of the state manager:
var state_manager = new HeadOfState();
3) Use the state_manager
instance to interact with the state:
var state_1 = state_manager.getState();
state_manager.addListener('change', function(latest_state) {
console.log(latest_state.a === 1); //true
console.log(state_manager.currentStateIsSameAs(latest_state)); // true
console.log(state_manager.currentStateIsSameAs(state_1)); // false
});
state_manager.setState('a', 1);
API
new HeadOfState(initial_state = {})
Creates a new state manager instance. If no initial state is supplied, an empty object is used:
var state_manager = new HeadOfState();
var state = state_manager.getState();
console.log(Object.keys(state) === 0); // true
Or, an initial state may be supplied:
var initial = {
a: 1,
b: 'two',
c: ['a', 'b', 'c'],
d: {},
e: false,
f: null
};
var state_manager = new HeadOfState(initial);
var state = state_manager.getState();
console.log(state.a === initial.a); //true
console.log(state.b === initial.b); //true
console.log(state.c[0] === initial.c[0]); //true
// etc.
Supplying initial states gives you the power to replay states, go back to previous states, load states from a database or cookie, etc.
state_manager.getState()
Gets a copy of the current state:
var state_manager = new HeadOfState();
var state = state_manager.getState();
state_manager.setState(key[, value])
Sets a portion of the state.
If key
is a string or an array, it can be used as a path to set a value on:
var initial = {
a: 1,
b: 'two',
c: ['a', 'b', 'c']
};
var state_manager = new HeadOfState(initial);
state_manager.setState('a', 2);
state_manager.setState('c[0]', 'hello');
var state = state_manager.getState();
console.log(state.a === 2); //true
console.log(state.c[0] === 'hello'); //true
If key
is an object, it can be used to assign multiple properties:
var initial = {
a: 1,
b: 'two',
c: ['a', 'b', 'c']
};
var state_manager = new HeadOfState(initial);
var new_a = 2;
var new_c = ['five', 'six'];
state_manager.setState({
a: new_a,
c: new_c
});
var state = state_manager.getState();
console.log(state.a === new_a); //true
console.log(state.b === initial.b); //true
console.log(state.c[0] === new_c[0]); //true
console.log(state.c[1] === new_c[1]); //true
console.log(state.c.length === new_c.length); //true
Note: supplying an empty object as the key
will not replace the state, it will simply not set anything on the current state.
state_manager.currentStateIsSameAs(state)
Checks if the current state is the same as the supplied state:
var state_manager = new HeadOfState();
var state_1 = state_manager.getState();
state_manager.setState('b', 2);
var state_2 = state_manager.getState();
console.log(state_manager.currentStateIsSameAs(state_1)); //false
console.log(state_manager.currentStateIsSameAs(state_2)); //true
state_manager.addListener("change", callback)
HeadOfState is also an instance of fbemitter, and can be used as such. Particularly, when the state changes, the state manager emits a "change" event:
var state_manager = new HeadOfState();
state_manager.addListener('change', function(latest_state) {
console.log(latest_state.b === 'three'); //true
console.log(state_manager.currentStateIsSameAs(latest_state)); //true
});
var state_1 = state_manager.getState();
console.log(state_manager.currentStateIsSameAs(state_1)); //true
state_manager.setState('b', 'three');
Usage in React
React Flux libraries are a dime-a-dozen. There are so many choices, and picking one can be confusing, with their custom methodologies and implementations. Worse, who knows when support will run out for the library you've picked?
Head of State is different, in that it is not a Flux implementation, but rather a simple tool that does one thing well: it manages state. However, combining it with React, you can avoid using large/confusing/flaky flux libraries by sticking to a very simple architecture:
- Set up "controller-view" components, which are the top/root-level components that get mounted first.
- Have controller-view states be intimately tied to the application-level state.
- Pass the controller-view states along as immutable props to the child components.
- Child components can make requests to change the application-level state.
- The controller-views should listen for application-level state changes, and call their own
setState
accordingly.
Example
In this example, ExamplePage
is the controller-view.
It uses a state manager to tie its state to the application-level state.
It passes its state properties along to its children as props.
Header
and Form
are two child components that utilize the "name" property of the application-level state.
Form can request to change the "name" property of the application-level state.
The ExamplePage
controller-view is listening to changes to the application-level state and updates its own state, so when Form
updates the name, ExamplePage
re-renders itself and its children.
var React = require('react');
var ReactDOM = require('react-dom');
var HeadOfState = require('head-of-state');
/**
* Child component.
*/
class Header extends React.Component {
constructor(props) {
super(props);
}
/**
* Use the name prop.
*
*/
render() {
return (
<header>
<p>Hello, {this.props.name}</p>
</header>
);
}
}
/**
* Child component.
*/
class Form extends React.Component {
constructor(props) {
super(props);
this.onChangeName = this.onChangeName.bind(this);
}
/**
* Set the application-level state to reflect the name change.
*
*/
onChangeName(e) {
this.props.state_manager.setState({
name: e.currentTarget.value
});
}
/**
* Use the name prop.
*/
render() {
return (
<form>
<input onChange={this.onChangeName} value={this.props.name} />
</form>
);
}
}
/**
* Controller-view.
*/
class ExamplePage extends React.Component {
constructor(props) {
super(props);
this.state = {
name: 'Shaun'
};
this.state_manager = new HeadOfState(this.state);
}
/**
* Set up the listener for the state change.
*/
componentDidMount() {
this.listener = this.state_manager.on('change', (state) => {
this.setState(state);
});
}
/**
* Remove the listener.
*/
componentWillUnmount() {
this.listener.remove();
}
/**
* Only update if the state has changed.
*/
shouldComponentUpdate(next_props, next_state) {
return !this.state_manager.currentStateIsSameAs(next_state);
}
/**
* Pass the state along as immutable props.
*/
render() {
return (
<div>
<Header name={this.state.name} />
<Form name={this.state.name} state_manager={this.state_manager} />
</div>
);
}
}
ReactDOM.render(
React.createElement(ExamplePage),
document.getElementById('controller-view')
);
It is worth noting that with this architecture, having an application-level state does not necessarily mean that a child component should not have its own state. Instead, it means that an application-level state is possible, and useful, as it can describe the application as a whole. You can describe as much or as little of your application at this level as you desire.
Also note that instead of passing down the application-level state as props, you may also call state_manager.getState()
in the child component's render
method to get the current application-level state.
Usage without React
This class has no React-specific elements, and therefore can be implemented into any type of project.
Wait, are the state manager's states mutable?
No, and yes. Instead of opting for strict immutability a la immutable.js, the states that the state manager returns from a call to getState
are actually plain javascript objects, which are copies of the actual current state.
This provides the flexibility of working with regular objects, with the power of immutability (since the actual application-level state is not directly changed).
Example:
var initial = {
a: 1
};
var state_manager = new HeadOfState(initial);
var state = state_manager.getState();
state.a = 2;
console.log(state.a === 2); //true
state = state_manager.getState();
console.log(state.a === 2); //false
console.log(state.a === initial.a); //true
Tests
Node.js
Run npm test
.
Browser
Open test/index.html
in your browser.