Have ideas to improve npm?Join in the discussion! »

politic

0.1.7 • Public • Published

Politic

Build Status

Predictable and observable state containers.

adjective: pol·i·tic

  • (of an action) seeming sensible and judicious under the circumstances.

Project Currently ALPHA!!! Use at your own risk.

Getting Started

Install and save this module as a dependency.

npm install politic --save

Really Simple

import Store from 'politic'
 
const store = new Store();
 
// The state starts as null if no initial state is passed to the constructor.
console.log(store.state); // null
 
// If no actions map is passed to the constructor, then "set" and "merge" are provided as defaults.
store.action('set', { foo: 1 })
store.action('merge', { bar: 2 });
 
// Actions are applied synchronously.
console.log(store.state); // { foo: 1, bar: 2 }
 
// Update notifications are asynchronous.
store.subscribe(state => {
    console.log(state); // { foo: 1, bar: 2 }
});

TODO List

This is a more "realistic" TODO list example.

File: TodoStore.js

import Store from 'politic'
 
export default new Store({
    // Initial state.
    state: { 
        items: []
    },
    
    // Actions map.
    actions: {
        add: (state, item) => {
            return Object.assign({}, state, {
                items: state.items.concat({
                    id: item.id,
                    value: ""+item.value,
                    completed: !!item.completed
                })
            });
        },
        
        removeId: (state, id) => {
            return Object.assign({}, state, {
                items: state.items.filter(item => item.id !== id)
            });
        },
        
        completeId: (state, id) => {
            return Object.assign({}, state, {
                items: state.items.map(item => {
                    return item.id !== id ? item : Object.assign({}, item, {
                        complete: true
                    });
                })
            });
        }
    },
    
    // Number of history states to keep for replay.
    historySize: 10
});

Create some subjects which represent abstract events in your TODO app.

File: Subjects.js

import { Subject } from 'politic'
 
export default {
    newItem: new Subject(),
    completeItem: new Subject(),
    removeItem: new Subject(),
    undo: new Subject()
};

Connect the subjects to the store.

File: Routing.js

import { STORE_REPLAY } from 'politic'
import todos from './TodoStore.js'
import { newItem, completeItem, removeItem, undo } from './Subjects.js'
 
function getItemId(item) {
    return item.id;
}
 
todos.connect('add', newItem);
todos.connect('completeId', completeItem, getItemId);
todos.connect('removeId', removeItem, getItemId);
todos.connect(STORE_REPLAY, undo, value => -value);

Use the subjects to cause state changes.

import { newItem, completeItem, removeItem, undo } from './Subjects.js'
 
let item = {
    id: 0,
    value: "Hello, World!"
};
 
// Add an incomplete todo.
newItem.publish(item);
 
// Complete the todo.
completeItem.publish(item);
 
// Remove the todo.
removeItem.publish(item);
 
// Undo one state change (removing the todo).
undo.publish(1);

Handle todo items updates.

import todos from './TodoStore.js'
 
todos.subscribe(state => {
    state.items.forEach(item => {
        console.log(item.value);
    });
});

Class: Store

Observable state container.

Pass values to predefined actions which use reducers to create the next state of the store. Store subscribers will be notified of updates to the Store's state.

Constructor: new Store(initialState, [actions])

  • initialState <any> The initial state of the store.
  • actions <Object> A map of action "reducer" methods. Default: DefaultActions

NOTE: The initialState object, actions map, and the new state object returned by reducers, will be recursively frozen.

Class Getter: store.state <any>

Get the current (readonly) state.

Class Method: store.history(offset) <any>

Get a (readonly) state from the Store's history.

This does not change the current state.

  • offset <Number> 0 is equivalent to store.state, positive gets a "redo" state, and negative gets an "undo" state.

Class Method: store.action(action, value)

Apply a value to the state using the action.

  • action <String> Name of the action to be applied.
  • value <any> Value to be applied to the Store's state by the action.
  • Returns: <any> New (readonly) state after the action is applied.

NOTE: An action cannot be applied as the direct result of an action or of an observable callback being invoked. This is to prevent state cascades, shared state responsibility, and complicated application flow.

Class Method: store.subscribe(callback)

Register a callback which will be invoked when the state is updated.

  • callback <Function> A function to be invoked when the store's state is updated. The callback will be passed the current (readonly) state.
  • Returns: <Function> Unsubscribe callback.

Class Method: store.connect(action, observable[, mapFunction])

Connect an observable (e.g. a Subject) so that values published by the observable are applied to the Store's state by using the action.

  • action <String> Action name.
  • observable <observable> Observable value source.
  • map <Function> Function used to transform published values before invoking the action. Default: identity
  • Returns: <Function> Disconnect callback.

NOTE: Even though Stores themselves are observable, you cannot connect one Store to another store. If a Store is passed to the connect() method, an error will be thrown. This is to prevent state cascades, shared state responsibility, and complicated application flow.

Using a Store's connect() method is an inversion of control, equivalent to using an observable's subscribe() method.

Example: Using store.connect()

store.connect(action, observable, mapFunction);

Example: Using observable.subscribe()

observable.subscribe(value => store.action(action, mapFunction(value)));

Class Method: store.copy()

Get a copy of the current Store with the same state and actions. History and current history state are also maintained. Changes to the copy will not affect the current Store. The copy will initially have no subscribers.

  • Returns: <Store> A copy of the current store.

Class: Subject

Observable event.

Publish values which will be sent to Subject subscribers. All subscribers will receive all values.

Subjects differ from Stores in that they have no internal state. Also, publishing a value will notify subscribers 1:1, whereas Stores may only notify subscribers of the final state after several actions have modified the state. Subjects are useful for decoupling events from state updates.

See the Subject.mixin() method for creating custom observable objects.

Constructor: new Subject()

Class Method: subject.publish(value)

Publish a value which will be sent to all observers.

NOTE: A value cannot be published as the direct result of an observable callback being invoked. This is to prevent event cascades and complicated application flow.

Class Method: subject.subscribe(callback)

Register a callback which will be invoked when a value is published.

  • callback <Function> A function to be invoked when a value is published.

  • value <Any>

Static Method: Subject.mixin(target)

Make the target object observable by adding a subscribe() method, backed by a new Subject instance.

  • target <Object> Object to be made observable.
  • Returns: <Subject> The new Subject instance.

FAQ

What does this buy me over Redux and the various connector libraries?

Coffee? But seriously, maybe nothing. I wanted something an order of magnitude simpler than Redux, it didn’t seem to exist, so I wrote it. Redux is arguably more powerful, but I didn’t need a lightsaber, I needed a screwdriver.

How does this compare with MobX?

It’s simpler. For instance, no React component wrapping. If you want a React component to update, then you’ll need to setState or re-render it in a Store subscription.

This is intentional because:

  • Simplicity - In this case, the core competency is state management. Helpers are fine, but they belong in another library, and hopefully can be useful for more than just Politic.
  • Visibility - I believe there is such a thing as too much help, and that less typing should not always be the goal. When you add helpers which eliminate some typing, you are also:
    • Making assumptions about what your consumer wants.
    • Making it harder for someone unfamiliar with your library to see how things are happening.
  • Independence - It has no knowledge of React or any other framework. No assumptions are made about how you will be using this library. Only that you need a predictable state.

What are Subjects and what are they for?

Subjects are stateless, observable, events. They are there for (optionally) making state changes declarative instead of imperative. Instead of having your UI or some other datasource call actions directly, have them publish to Subjects. Then you can wire up Subjects to one or more Stores, using one or more actions. Now your inputs don't have to know how their values will be handled. They only have to declare that there is a value, or that an event has happened.

Are hierarchies of models supported?

Short answer, yes. You can store anything you want as state. But, if you mean, is Politic aware of special things like observables in it's state, then no. This would put restrictions on what a state is and how it can be manipulated. Politic is designed to make as few assumptions as possible around what it will contain and how it will be used.

As compensation, you can have as many stores as you want. You might also look at Subjects as a way to keep multiple stores in sync.

Install

npm i [email protected]

Version

0.1.7

License

MIT

Last publish

Collaborators

  • avatar