head-of-state

1.0.2 • Public • Published

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>
  1. Create an instance of the state manager:
var state_manager = new HeadOfState();
  1. 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:

  1. Set up "controller-view" components, which are the top/root-level components that get mounted first.
  2. Have controller-view states be intimately tied to the application-level state.
  3. Pass the controller-view states along as immutable props to the child components.
  4. Child components can make requests to change the application-level state.
  5. 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.

Package Sidebar

Install

npm i head-of-state

Weekly Downloads

0

Version

1.0.2

License

MIT

Last publish

Collaborators

  • shaunpersad