1import $$observable from './utils/symbol-observable' 2 3import { 4 Store, 5 PreloadedState, 6 StoreEnhancer, 7 Dispatch, 8 Observer, 9 ExtendState 10} from './types/store' 11import { Action } from './types/actions' 12import { Reducer } from './types/reducers' 13import ActionTypes from './utils/actionTypes' 14import isPlainObject from './utils/isPlainObject' 15import { kindOf } from './utils/kindOf' 16 17/** 18 * Creates a Redux store that holds the state tree. 19 * The only way to change the data in the store is to call `dispatch()` on it. 20 * 21 * There should only be a single store in your app. To specify how different 22 * parts of the state tree respond to actions, you may combine several reducers 23 * into a single reducer function by using `combineReducers`. 24 * 25 * @param reducer A function that returns the next state tree, given 26 * the current state tree and the action to handle. 27 * 28 * @param preloadedState The initial state. You may optionally specify it 29 * to hydrate the state from the server in universal apps, or to restore a 30 * previously serialized user session. 31 * If you use `combineReducers` to produce the root reducer function, this must be 32 * an object with the same shape as `combineReducers` keys. 33 * 34 * @param enhancer The store enhancer. You may optionally specify it 35 * to enhance the store with third-party capabilities such as middleware, 36 * time travel, persistence, etc. The only store enhancer that ships with Redux 37 * is `applyMiddleware()`. 38 * 39 * @returns A Redux store that lets you read the state, dispatch actions 40 * and subscribe to changes. 41 */ 42export default function createStore< 43 S, 44 A extends Action, 45 Ext = {}, 46 StateExt = never 47>( 48 reducer: Reducer<S, A>, 49 enhancer?: StoreEnhancer<Ext, StateExt> 50): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext 51export default function createStore< 52 S, 53 A extends Action, 54 Ext = {}, 55 StateExt = never 56>( 57 reducer: Reducer<S, A>, 58 preloadedState?: PreloadedState<S>, 59 enhancer?: StoreEnhancer<Ext, StateExt> 60): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext 61export default function createStore< 62 S, 63 A extends Action, 64 Ext = {}, 65 StateExt = never 66>( 67 reducer: Reducer<S, A>, 68 preloadedState?: PreloadedState<S> | StoreEnhancer<Ext, StateExt>, 69 enhancer?: StoreEnhancer<Ext, StateExt> 70): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext { 71 if ( 72 (typeof preloadedState === 'function' && typeof enhancer === 'function') || 73 (typeof enhancer === 'function' && typeof arguments[3] === 'function') 74 ) { 75 throw new Error( 76 'It looks like you are passing several store enhancers to ' + 77 'createStore(). This is not supported. Instead, compose them ' + 78 'together to a single function. See https://redux.js.org/tutorials/fundamentals/part-4-store#creating-a-store-with-enhancers for an example.' 79 ) 80 } 81 82 if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') { 83 enhancer = preloadedState as StoreEnhancer<Ext, StateExt> 84 preloadedState = undefined 85 } 86 87 if (typeof enhancer !== 'undefined') { 88 if (typeof enhancer !== 'function') { 89 throw new Error( 90 `Expected the enhancer to be a function. Instead, received: '${kindOf( 91 enhancer 92 )}'` 93 ) 94 } 95 96 return enhancer(createStore)( 97 reducer, 98 preloadedState as PreloadedState<S> 99 ) as Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext 100 } 101 102 if (typeof reducer !== 'function') { 103 throw new Error( 104 `Expected the root reducer to be a function. Instead, received: '${kindOf( 105 reducer 106 )}'` 107 ) 108 } 109 110 let currentReducer = reducer 111 let currentState = preloadedState as S 112 let currentListeners: (() => void)[] | null = [] 113 let nextListeners = currentListeners 114 let isDispatching = false 115 116 /** 117 * This makes a shallow copy of currentListeners so we can use 118 * nextListeners as a temporary list while dispatching. 119 * 120 * This prevents any bugs around consumers calling 121 * subscribe/unsubscribe in the middle of a dispatch. 122 */ 123 function ensureCanMutateNextListeners() { 124 if (nextListeners === currentListeners) { 125 nextListeners = currentListeners.slice() 126 } 127 } 128 129 /** 130 * Reads the state tree managed by the store. 131 * 132 * @returns The current state tree of your application. 133 */ 134 function getState(): S { 135 if (isDispatching) { 136 throw new Error( 137 'You may not call store.getState() while the reducer is executing. ' + 138 'The reducer has already received the state as an argument. ' + 139 'Pass it down from the top reducer instead of reading it from the store.' 140 ) 141 } 142 143 return currentState as S 144 } 145 146 /** 147 * Adds a change listener. It will be called any time an action is dispatched, 148 * and some part of the state tree may potentially have changed. You may then 149 * call `getState()` to read the current state tree inside the callback. 150 * 151 * You may call `dispatch()` from a change listener, with the following 152 * caveats: 153 * 154 * 1. The subscriptions are snapshotted just before every `dispatch()` call. 155 * If you subscribe or unsubscribe while the listeners are being invoked, this 156 * will not have any effect on the `dispatch()` that is currently in progress. 157 * However, the next `dispatch()` call, whether nested or not, will use a more 158 * recent snapshot of the subscription list. 159 * 160 * 2. The listener should not expect to see all state changes, as the state 161 * might have been updated multiple times during a nested `dispatch()` before 162 * the listener is called. It is, however, guaranteed that all subscribers 163 * registered before the `dispatch()` started will be called with the latest 164 * state by the time it exits. 165 * 166 * @param listener A callback to be invoked on every dispatch. 167 * @returns A function to remove this change listener. 168 */ 169 function subscribe(listener: () => void) { 170 if (typeof listener !== 'function') { 171 throw new Error( 172 `Expected the listener to be a function. Instead, received: '${kindOf( 173 listener 174 )}'` 175 ) 176 } 177 178 if (isDispatching) { 179 throw new Error( 180 'You may not call store.subscribe() while the reducer is executing. ' + 181 'If you would like to be notified after the store has been updated, subscribe from a ' + 182 'component and invoke store.getState() in the callback to access the latest state. ' + 183 'See https://redux.js.org/api/store#subscribelistener for more details.' 184 ) 185 } 186 187 let isSubscribed = true 188 189 ensureCanMutateNextListeners() 190 nextListeners.push(listener) 191 192 return function unsubscribe() { 193 if (!isSubscribed) { 194 return 195 } 196 197 if (isDispatching) { 198 throw new Error( 199 'You may not unsubscribe from a store listener while the reducer is executing. ' + 200 'See https://redux.js.org/api/store#subscribelistener for more details.' 201 ) 202 } 203 204 isSubscribed = false 205 206 ensureCanMutateNextListeners() 207 const index = nextListeners.indexOf(listener) 208 nextListeners.splice(index, 1) 209 currentListeners = null 210 } 211 } 212 213 /** 214 * Dispatches an action. It is the only way to trigger a state change. 215 * 216 * The `reducer` function, used to create the store, will be called with the 217 * current state tree and the given `action`. Its return value will 218 * be considered the **next** state of the tree, and the change listeners 219 * will be notified. 220 * 221 * The base implementation only supports plain object actions. If you want to 222 * dispatch a Promise, an Observable, a thunk, or something else, you need to 223 * wrap your store creating function into the corresponding middleware. For 224 * example, see the documentation for the `redux-thunk` package. Even the 225 * middleware will eventually dispatch plain object actions using this method. 226 * 227 * @param action A plain object representing “what changed”. It is 228 * a good idea to keep actions serializable so you can record and replay user 229 * sessions, or use the time travelling `redux-devtools`. An action must have 230 * a `type` property which may not be `undefined`. It is a good idea to use 231 * string constants for action types. 232 * 233 * @returns For convenience, the same action object you dispatched. 234 * 235 * Note that, if you use a custom middleware, it may wrap `dispatch()` to 236 * return something else (for example, a Promise you can await). 237 */ 238 function dispatch(action: A) { 239 if (!isPlainObject(action)) { 240 throw new Error( 241 `Actions must be plain objects. Instead, the actual type was: '${kindOf( 242 action 243 )}'. You may need to add middleware to your store setup to handle dispatching other values, such as 'redux-thunk' to handle dispatching functions. See https://redux.js.org/tutorials/fundamentals/part-4-store#middleware and https://redux.js.org/tutorials/fundamentals/part-6-async-logic#using-the-redux-thunk-middleware for examples.` 244 ) 245 } 246 247 if (typeof action.type === 'undefined') { 248 throw new Error( 249 'Actions may not have an undefined "type" property. You may have misspelled an action type string constant.' 250 ) 251 } 252 253 if (isDispatching) { 254 throw new Error('Reducers may not dispatch actions.') 255 } 256 257 try { 258 isDispatching = true 259 currentState = currentReducer(currentState, action) 260 } finally { 261 isDispatching = false 262 } 263 264 const listeners = (currentListeners = nextListeners) 265 for (let i = 0; i < listeners.length; i++) { 266 const listener = listeners[i] 267 listener() 268 } 269 270 return action 271 } 272 273 /** 274 * Replaces the reducer currently used by the store to calculate the state. 275 * 276 * You might need this if your app implements code splitting and you want to 277 * load some of the reducers dynamically. You might also need this if you 278 * implement a hot reloading mechanism for Redux. 279 * 280 * @param nextReducer The reducer for the store to use instead. 281 * @returns The same store instance with a new reducer in place. 282 */ 283 function replaceReducer<NewState, NewActions extends A>( 284 nextReducer: Reducer<NewState, NewActions> 285 ): Store<ExtendState<NewState, StateExt>, NewActions, StateExt, Ext> & Ext { 286 if (typeof nextReducer !== 'function') { 287 throw new Error( 288 `Expected the nextReducer to be a function. Instead, received: '${kindOf( 289 nextReducer 290 )}` 291 ) 292 } 293 294 // TODO: do this more elegantly 295 ;(currentReducer as unknown as Reducer<NewState, NewActions>) = nextReducer 296 297 // This action has a similar effect to ActionTypes.INIT. 298 // Any reducers that existed in both the new and old rootReducer 299 // will receive the previous state. This effectively populates 300 // the new state tree with any relevant data from the old one. 301 dispatch({ type: ActionTypes.REPLACE } as A) 302 // change the type of the store by casting it to the new store 303 return store as unknown as Store< 304 ExtendState<NewState, StateExt>, 305 NewActions, 306 StateExt, 307 Ext 308 > & 309 Ext 310 } 311 312 /** 313 * Interoperability point for observable/reactive libraries. 314 * @returns A minimal observable of state changes. 315 * For more information, see the observable proposal: 316 * https://github.com/tc39/proposal-observable 317 */ 318 function observable() { 319 const outerSubscribe = subscribe 320 return { 321 /** 322 * The minimal observable subscription method. 323 * @param observer Any object that can be used as an observer. 324 * The observer object should have a `next` method. 325 * @returns An object with an `unsubscribe` method that can 326 * be used to unsubscribe the observable from the store, and prevent further 327 * emission of values from the observable. 328 */ 329 subscribe(observer: unknown) { 330 if (typeof observer !== 'object' || observer === null) { 331 throw new TypeError( 332 `Expected the observer to be an object. Instead, received: '${kindOf( 333 observer 334 )}'` 335 ) 336 } 337 338 function observeState() { 339 const observerAsObserver = observer as Observer<S> 340 if (observerAsObserver.next) { 341 observerAsObserver.next(getState()) 342 } 343 } 344 345 observeState() 346 const unsubscribe = outerSubscribe(observeState) 347 return { unsubscribe } 348 }, 349 350 [$$observable]() { 351 return this 352 } 353 } 354 } 355 356 // When a store is created, an "INIT" action is dispatched so that every 357 // reducer returns their initial state. This effectively populates 358 // the initial state tree. 359 dispatch({ type: ActionTypes.INIT } as A) 360 361 const store = { 362 dispatch: dispatch as Dispatch<A>, 363 subscribe, 364 getState, 365 replaceReducer, 366 [$$observable]: observable 367 } as unknown as Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext 368 return store 369} 370