UNPKG

5.95 kBPlain TextView Raw
1import * as Redux from 'redux'
2import {
3 Action,
4 ConfigRedux,
5 ModelReducers,
6 NamedModel,
7 RematchBag,
8 DevtoolOptions,
9 Models,
10 RematchRootState,
11} from './types'
12
13/**
14 * Creates 'combined' reducer for each model and then merges those reducers
15 * together into a 'root' reducer. It then creates a Redux store with
16 * middlewares and enhancers.
17 */
18export default function createReduxStore<
19 TModels extends Models<TModels>,
20 TExtraModels extends Models<TModels>,
21 RootState = RematchRootState<TModels, TExtraModels>
22>(bag: RematchBag<TModels, TExtraModels>): Redux.Store<RootState> {
23 bag.models.forEach((model) => createModelReducer(bag, model))
24
25 const rootReducer = createRootReducer<RootState, TModels, TExtraModels>(bag)
26
27 const middlewares = Redux.applyMiddleware(...bag.reduxConfig.middlewares)
28 const enhancers = bag.reduxConfig.devtoolComposer
29 ? bag.reduxConfig.devtoolComposer(...bag.reduxConfig.enhancers, middlewares)
30 : composeEnhancersWithDevtools(bag.reduxConfig.devtoolOptions)(
31 ...bag.reduxConfig.enhancers,
32 middlewares
33 )
34
35 const createStore = bag.reduxConfig.createStore || Redux.createStore
36 const bagInitialState = bag.reduxConfig.initialState
37 const initialState = bagInitialState === undefined ? {} : bagInitialState
38
39 return createStore<RootState, Action, any, typeof initialState>(
40 rootReducer,
41 initialState,
42 enhancers
43 )
44}
45
46/**
47 * Creates a combined reducer for a given model. What it means is that:
48 * - it forms an action name for each model's reducer as 'modelName/reducerKey'
49 * - it creates a mapping from action name to its reducer
50 * - it wraps the mapping with a function (combined reducer) that selects and
51 * runs a reducer based on the incoming action
52 * - if the model also has a base reducer defined, it creates a function which
53 * first runs the incoming action through this reducer and then passes the
54 * resulting state and the same action to combined reducer
55 *
56 * The final result - a function, is returned.
57 */
58export function createModelReducer<
59 TModels extends Models<TModels>,
60 TExtraModels extends Models<TModels>,
61 TState extends NamedModel<TModels>['state'] = any
62>(bag: RematchBag<TModels, TExtraModels>, model: NamedModel<TModels>): void {
63 const modelReducers: ModelReducers<TState> = {}
64
65 // build action name for each reducer and create mapping from name to reducer
66 const modelReducerKeys = Object.keys(model.reducers)
67 modelReducerKeys.forEach((reducerKey) => {
68 const actionName = isAlreadyActionName(reducerKey)
69 ? reducerKey
70 : `${model.name}/${reducerKey}`
71
72 modelReducers[actionName] = model.reducers[reducerKey]
73 })
74
75 // select and run a reducer based on the incoming action
76 const combinedReducer = (
77 state: TState = model.state,
78 action: Action
79 ): TState => {
80 if (action.type in modelReducers) {
81 return modelReducers[action.type](
82 state,
83 action.payload,
84 action.meta
85 // we use augmentation because a reducer can return void due immer plugin,
86 // which makes optional returning the reducer state
87 ) as TState
88 }
89
90 return state
91 }
92
93 const modelBaseReducer = model.baseReducer
94
95 // when baseReducer is defined, run the action first through it
96 let reducer = !modelBaseReducer
97 ? combinedReducer
98 : (state: TState = model.state, action: Action): TState =>
99 combinedReducer(modelBaseReducer(state, action), action)
100
101 bag.forEachPlugin('onReducer', (onReducer) => {
102 reducer = onReducer(reducer, model.name, bag) || reducer
103 })
104
105 bag.reduxConfig.reducers[model.name] = reducer
106}
107
108/**
109 * It merges all reducers in config using mergeReducers function. Additionally,
110 * if user supplied any rootReducers, a wrapper function around merged reducers
111 * is created. It first feeds each into its corresponding 'root' reducer (if
112 * it's available), and then passes on the resulting state to the merged reducer.
113 */
114export function createRootReducer<
115 TRootState,
116 TModels extends Models<TModels>,
117 TExtraModels extends Models<TModels>
118>(bag: RematchBag<TModels, TExtraModels>): Redux.Reducer<TRootState, Action> {
119 const { rootReducers } = bag.reduxConfig
120 const mergedReducers = mergeReducers<TRootState>(bag.reduxConfig)
121 let rootReducer = mergedReducers
122
123 if (rootReducers && Object.keys(rootReducers).length) {
124 rootReducer = (
125 state: TRootState | undefined,
126 action: Action
127 ): TRootState => {
128 const actionRootReducer = rootReducers[action.type]
129
130 if (actionRootReducer) {
131 return mergedReducers(actionRootReducer(state, action), action)
132 }
133
134 return mergedReducers(state, action)
135 }
136 }
137
138 bag.forEachPlugin('onRootReducer', (onRootReducer) => {
139 rootReducer = onRootReducer(rootReducer, bag) || rootReducer
140 })
141
142 return rootReducer
143}
144
145/**
146 * Merges all reducers defined in config into one function using user supplied
147 * or default combineReducers function.
148 * If there are no reducers defined, it returns a function that just returns
149 * the state for all incoming actions.
150 */
151function mergeReducers<TRootState>(
152 reduxConfig: ConfigRedux<TRootState>
153): Redux.Reducer<TRootState, Action> {
154 const combineReducers = reduxConfig.combineReducers || Redux.combineReducers
155
156 if (!Object.keys(reduxConfig.reducers).length) {
157 return (state: any): TRootState => state
158 }
159
160 return combineReducers(reduxConfig.reducers as Redux.ReducersMapObject)
161}
162
163/**
164 * Returns Redux Devtools compose method unless it's disabled, in which case it
165 * returns default Redux.compose.
166 */
167function composeEnhancersWithDevtools(
168 devtoolOptions: DevtoolOptions = {}
169): (...args: any[]) => Redux.StoreEnhancer {
170 return !devtoolOptions.disabled &&
171 typeof window === 'object' &&
172 window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
173 ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__(devtoolOptions)
174 : Redux.compose
175}
176
177/**
178 * Determines if a reducer key is already an action name, for example -
179 * a listener on another model.
180 */
181function isAlreadyActionName(reducerKey: string): boolean {
182 return reducerKey.indexOf('/') > -1
183}