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