/
unistore.js
172 lines (149 loc) 路 5.34 KB
/
unistore.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
import { h, Component } from 'preact';
/** Creates a new store, which is a tiny evented state container.
* @name createStore
* @param {Object} [state={}] Optional initial state
* @returns {store}
* @example
* let store = createStore();
* store.subscribe( state => console.log(state) );
* store.setState({ a: 'b' }); // logs { a: 'b' }
* store.setState({ c: 'd' }); // logs { a: 'b', c: 'd' }
*/
export function createStore(state) {
let listeners = [];
state = state || {};
function unsubscribe(listener) {
let i = listeners.indexOf(listener);
listeners.splice(i, !!~i);
}
/** An observable state container, returned from {@link createStore}
* @name store
*/
return /** @lends store */ {
/** Apply a partial state object to the current state, invoking registered listeners.
* @param {Object} update An object with properties to be merged into state
* @param {Boolean} [overwrite=false] If `true`, update will replace state instead of being merged into it
*/
setState(update, overwrite) {
state = overwrite ? update : assign(assign({}, state), update);
for (let i=0; i<listeners.length; i++) listeners[i](state);
},
/** Register a listener function to be called whenever state is changed.
* @param {Function} listener A function to call when state changes. Gets passed the new state.
* @returns {Function} unsubscribe()
*/
subscribe(listener) {
listeners.push(listener);
return () => { unsubscribe(listener); };
},
/** Remove a previously-registered listener function.
* @param {Function} listener The callback previously passed to `subscribe()` that should be removed.
* @function
*/
unsubscribe,
/** Retrieve the current state object.
* @returns {Object} state
*/
getState() {
return state;
}
};
}
/** Wire a component up to the store. Passes state as props, re-renders on change.
* @param {Function|Array|String} mapStateToProps A function mapping of store state to prop values, or an array/CSV of properties to map.
* @param {Function|Object} [actions] Action functions (pure state mappings), or a factory returning them. Every action function gets current state as the first parameter and any other params next
* @returns {Component} ConnectedComponent
* @example
* const Foo = connect('foo,bar')( ({ foo, bar }) => <div /> )
* @example
* const actions = { someAction }
* const Foo = connect('foo,bar', actions)( ({ foo, bar, someAction }) => <div /> )
* @example
* @connect( state => ({ foo: state.foo, bar: state.bar }) )
* export class Foo { render({ foo, bar }) { } }
*/
export function connect(mapStateToProps, actions) {
if (typeof mapStateToProps!=='function') {
mapStateToProps = select(mapStateToProps || []);
}
return Child => {
function Wrapper(props, { store }) {
let state = mapStateToProps(store ? store.getState() : {}, props);
let boundActions = actions ? mapActions(actions, store) : { store };
let update = () => {
let mapped = mapStateToProps(store ? store.getState() : {}, this.props);
if (!shallowEqual(mapped, state)) {
state = mapped;
this.setState(null);
}
};
this.componentDidMount = () => {
store.subscribe(update);
};
this.componentWillUnmount = () => {
store.unsubscribe(update);
};
this.render = props => h(Child, assign(assign(assign({}, boundActions), props), state));
}
return (Wrapper.prototype = new Component()).constructor = Wrapper;
};
}
/** Provider exposes a store (passed as `props.store`) into context.
*
* Generally, an entire application is wrapped in a single `<Provider>` at the root.
* @class
* @extends Component
* @param {Object} props
* @param {Store} props.store A {Store} instance to expose via context.
*/
export function Provider(){}
Provider.prototype.getChildContext = function() {
return { store: this.props.store };
};
Provider.prototype.render = function(props) {
return props.children[0];
};
// Bind an object/factory of actions to the store and wrap them.
function mapActions(actions, store) {
if (typeof actions==='function') actions = actions(store);
let mapped = {};
for (let i in actions) {
mapped[i] = createAction(store, actions[i]);
}
return mapped;
}
// Bind a single action to the store and sequester its return value.
// Performance tests verifying this is the best solution: https://esbench.com/bench/5a295e6299634800a0349500
function createAction(store, action) {
return function() {
let args = [store.getState()];
for (let i=0; i<arguments.length; i++) args.push(arguments[i]);
let ret = action.apply(store, args);
if (ret!=null) {
if (ret.then) ret.then(store.setState);
else store.setState(ret);
}
};
}
// select('foo,bar') creates a function of the form: ({ foo, bar }) => ({ foo, bar })
function select(properties) {
if (typeof properties==='string') properties = properties.split(',');
return state => {
let selected = {};
for (let i=0; i<properties.length; i++) {
selected[properties[i]] = state[properties[i]];
}
return selected;
};
}
// Returns a boolean indicating if all keys and values match between two objects.
function shallowEqual(a, b) {
for (let i in a) if (a[i]!==b[i]) return false;
for (let i in b) if (!(i in a)) return false;
return true;
}
// Lighter Object.assign stand-in
function assign(obj, props) {
for (let i in props) obj[i] = props[i];
return obj;
}