-
Notifications
You must be signed in to change notification settings - Fork 13
/
createReducer.js
97 lines (85 loc) · 3.27 KB
/
createReducer.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
import { handleAction } from 'redux-actions'
import reduceReducers from 'reduce-reducers'
import mapValues from 'lodash.mapvalues'
import values from 'lodash.values'
import reduce from 'lodash.reduce'
import filter from 'lodash.filter'
import { Map, Iterable } from 'immutable'
// terminology:
// container - an object that contains initialState + reducer functions
// initialState - the default state of a node and its children
const isFunction = v => typeof v === 'function'
const getInitialState = (o, ns) => {
return reduce(o, (prev, v, k) => {
if (k === 'initialState') return prev
const name = ns ? `${ns}.${k}` : k
if (typeof v === 'object') {
if (!Map.isMap(prev)) {
throw new Error(`Reducer "${ns || 'root'}" has a non-map initialState, so it can't have children`)
}
if (typeof prev.get(k) !== 'undefined') {
throw new Error(`Reducer "${ns || 'root'}" has an initialState conflict with it's parent over "${k}"`)
}
return prev.set(k, getInitialState(v, name))
}
return prev
}, o.initialState || Map())
}
const createReducerNode = ({ name, statePath, reducer, initialState }) =>
(state, action = {}) => {
// if we are the reducer container, pass them our cherry-picked state
// otherwise pass down the full state to the next container
const currNodeState = (statePath ? state.getIn(statePath) : state) || initialState
if (!Iterable.isIterable(currNodeState)) {
throw new Error(`Reducer "${name || 'root'}" was given a non-Immutable state!`)
}
const nextNodeState = reducer(currNodeState, action)
if (!Iterable.isIterable(nextNodeState)) {
throw new Error(`Reducer "${name || 'root'}" returned a non-Immutable state!`)
}
const nextRootState = statePath ? state.setIn(statePath, nextNodeState) : nextNodeState
return nextRootState
}
// recursively map reducers object to an
// array of reducers that handle namespaced actions
const createReducers = (o, parentName) => {
let hadReducers = false
const reducers = filter(mapValues(o, (v, k) => {
if (k === 'initialState') return
const name = parentName ? `${parentName}.${k}` : k
if (isFunction(v)) {
hadReducers = true
return handleAction(name, v)
}
if (typeof v === 'object') {
return createReducer(v, name)
}
}), isFunction)
return {
name: parentName,
isContainer: hadReducers,
reducers: reducers
}
}
const createReducer = (o, parentName) => {
const { reducers, isContainer, name } = createReducers(o, parentName)
if (isContainer && typeof o.initialState === 'undefined') {
throw new Error(`Reducer "${name || 'root'}" is missing an initialState`)
}
if (!isContainer && typeof o.initialState !== 'undefined') {
throw new Error(`Reducer "${name || 'root'}" has no reducers, so it can't specify an initialState`)
}
const initialState = getInitialState(o)
if (!Iterable.isIterable(initialState)) {
throw new Error(`Reducer "${name || 'root'}" is missing an Immutable initialState`)
}
const reducer = reduceReducers(...values(reducers))
const statePath = name && isContainer ? name.split('.') : undefined
return createReducerNode({
name: name,
initialState: initialState,
reducer: reducer,
statePath: statePath
})
}
export default createReducer