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