• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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