politic v0.3.0
Politic
Predictable and observable state containers.
See also, react-politic for React integration.
adjective: pol·i·tic
- (of an action) seeming sensible and judicious under the circumstances.
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) => {
const items = state.items;
return Object.assign({}, state, {
items: items.concat({
id: item.id,
value: ""+item.value,
completed: !!item.completed
})
});
},
removeId: (state, id) => {
const items = state.items;
return Object.assign({}, state, {
items: items.filter(item => item.id !== id)
});
},
completeId: (state, id) => {
const items = state.items;
return Object.assign({}, state, {
items: items.map(item => {
return item.id !== id ? item : Object.assign({}, item, {
complete: true
});
})
});
}
}
});
Create some RxJS Subjects (or equivalent ES Observables) which represent abstract events in your TODO app. Subjects can serve to invert control so that event and data sources do not need direct knowledge of store actions. They can also be used to enable asynchronous state changes.
File: Subjects.js
import Rx from "rxjs/Rx";
export default {
newItem: new Rx.Subject(),
completeItem: new Rx.Subject(),
removeItem: new Rx.Subject()
};
Connect the subjects to the store.
File: Routing.js
import todos from "./TodoStore";
import { newItem, completeItem, removeItem } from "./Subjects";
function getItemId(item) {
return item.id;
}
todos.connect('add', newItem);
todos.connect('completeId', completeItem, getItemId);
todos.connect('removeId', removeItem, getItemId);
Use the subjects to cause state changes.
import { newItem, completeItem, removeItem } from "./Subjects";
let item = {
id: 0,
value: "Hello, World!"
};
// Add an incomplete todo.
newItem.next(item);
// Complete the todo.
completeItem.next(item);
// Remove the todo.
removeItem.next(item);
Handle todo items updates.
import todos from "./TodoStore";
todos.subscribe(state => {
state.items.forEach(item => {
console.log(item.value);
});
});
Class: Store
State container which implements the ES Observable Proposal.
import Store from "politic";
Pass values to predefined actions which use reducers to create the next state of the store. Store subscribers will be notified of changes to the Store's state.
Constructor: new Store([options])
options
Object - Store options map. Default:{}
initialState
any - The initial state of the store. Default:null
actions
Object<String|Symbol, Action> - A map of action "reducer" methods. Default:DefaultActions
middleware
Array<Middleware> - An array of middlewares. Default:[]
NOTE: The initialState
value, actions
map, and new states returned by reducers, will be recursively frozen.
Property: store.state
any
The current (readonly) state.
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: If an action is invoked on the store as the result of a state change notification on the same store, an error will be thrown.
Actions are "lifted" to the store so that if an action name does not conflict with an existing store instance method
(e.g. subscribe
, connect
, toString
, etc.), then it can also be invoked as a store instance method (e.g.
store.action('foo', value)
is equivalent to store.foo(value)
).
Method: store.subscribe(observer)
Register an observer which will be notified when the store's state changes. Implements the ES Observable subscribe(...) method.
observer
Observer|Function - An observer oronNext
function to be subscribed to store state changes.- Returns: Subscription - A subscription which can be cancelled.
Method: store.connect(action, observable[, mapFunction])
Connect an observable so that published values are applied to the Store's state 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:value => value
- Returns: Subscription - A subscription which can be cancelled.
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(...)
const subscription = store.connect(action, observable, mapFunction);
Example: Using observable.subscribe(...)
const subscription = observable.subscribe(value => store.action(action, mapFunction(value)));
Method: store.copy()
Get a copy of the current Store with the same state and actions.
- Returns: Store - A copy of the current store.
Callback: Action(state, value)
Prototype for Politic action "reducer" functions. All properties of the actions
map passed to the Store constructor
should be functions with this prototype.
state
any - Current (readonly) state of the store.value
any - Action value.- Returns: any - The next state. If no state change is required, then return the
state
reference.
Object: DefaultActions
Default actions map used when no explicit actions map is defined for a new store.
import { DefaultActions } from "politic";
Property: set
Action
Replace the current state with the action value.
Property: merge
Action
Shallow merge (e.g. Object.assign({}, state, value)
) the action value with the current state.
If either the state or the new value is not an object, then the new value will replace the current state.
Middleware
Middleware are functions which wrap every action. They can be used to log state changes, filter actions or action values, and to validate or modify the state returned by actions.
See MIDDLEWARE.md for more information.
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.
Above all, Politic is designed to be simple and easy to integrate with any project.
Here's the "simplest" Redux use case:
import { createStore } from "redux";
const store = createStore((state, action) => {
if (action.type === 'merge') return Object.assign({}, state, action.payload);
});
store.dispatch({ type: 'merge', payload: { a: 1 } });
store.dispatch({ type: 'merge', payload: { b: 2 } });
store.dispatch({ type: 'merge', payload: { a: 3 } });
console.log(store.getState()); // { a: 3, b: 2 }
Here's the same for Politic:
import Store from "politic";
// Using the default "set" and "merge" actions.
const store = new Store();
// Actions are "lifted" to instance methods if they won't conflict with existing instance methods.
store.merge({ a: 1 });
store.merge({ b: 2 });
store.merge({ a: 3 });
// ES6 getter for state.
console.log(store.state); // { a: 3, b: 2 }
How does this compare with MobX?
MobX has "fine-grained" observables. Politic does not. Stores are observable, but they are not designed to be nested, and a subscriber gets the whole state every time.
Instead of reinventing the observable wheel, Politic leverages the ES Observable Proposal which makes it compatible with RxJS among others.
Politic probably has more in common with Redux. State modifications are the result of pre-defined Store actions, rather then direct manipulation of the state.
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.
How do I create asynchronous actions?
Asynchronous operations probably shouldn't be part of an action, because actions are supposed to be pure. However, you
could use middleware to accept Promise
objects returned from actions, and then asynchronously invoke another action
when the promise is resolved.
A better option would be to write or wrap your asynchronous operations as
ES Observables, then use the store.connect(...)
method to update your Store when the asynchronous operation concludes. RxJS is a good place
to start for creating observables.
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago