Initializing State
This page shows reducer patterns written by hand. See the Structuring Reducers intro for how they relate to Redux Toolkit's createSlice.
There are two main ways to initialize state for your application. configureStore accepts an optional preloadedState value (the same value is the second argument to the core createStore function). Reducers can also specify an initial value by looking for an incoming state argument that is undefined, and returning the value they'd like to use as a default. This can either be done with an explicit check inside the reducer, or by using the default argument value syntax: function myReducer(state = someDefaultValue, action).
It's not always immediately clear how these two approaches interact. Fortunately, the process does follow some predictable rules. Here's how the pieces fit together.
Summary
Without combineReducers() or similar manual code, preloadedState always wins over state = ... in the reducer because the state passed to the reducer is preloadedState and is not undefined, so the argument syntax doesn't apply.
With combineReducers() the behavior is more nuanced. Those reducers whose state is specified in preloadedState will receive that state. Other reducers will receive undefined and because of that will fall back to the state = ... default argument they specify.
In general, preloadedState wins over the state specified by the reducer. This lets reducers specify initial data that makes sense to them as default arguments, but also allows loading existing data (fully or partially) when you're hydrating the store from some persistent storage or the server.
Note: Reducers whose initial state is populated using preloadedState will still need to provide a default value to handle when passed a state of undefined. All reducers are passed undefined on initialization, so they should be written such that when given undefined, some value should be returned. This can be any non-undefined value; there's no need to duplicate the section of preloadedState here as the default.
In Depth
Single Simple Reducer
First let's consider a case where you have a single reducer. Say you don't use combineReducers().
Then your reducer might look like this:
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}
Now let's say you create a store with it.
import { configureStore } from '@reduxjs/toolkit'
const store = configureStore({ reducer: counter })
console.log(store.getState()) // 0
The initial state is zero. Why? Because no preloadedState was passed, so it was undefined. This is the state passed to your reducer the first time. When Redux initializes it dispatches a "dummy" action to fill the state. So your counter reducer was called with state equal to undefined. This is exactly the case that "activates" the default argument. Therefore, state is now 0 as per the default state value (state = 0). This state (0) will be returned.
Let's consider a different scenario:
import { configureStore } from '@reduxjs/toolkit'
const store = configureStore({ reducer: counter, preloadedState: 42 })
console.log(store.getState()) // 42
Why is it 42, and not 0, this time? Because 42 was passed as the preloadedState. This value becomes the state passed to your reducer along with the dummy action. This time, state is not undefined (it's 42!), so default argument syntax has no effect. The state is 42, and 42 is returned from the reducer.
Combined Reducers
Now let's consider a case where you use combineReducers().
You have two reducers:
function a(state = 'lol', action) {
return state
}
function b(state = 'wat', action) {
return state
}
The reducer generated by combineReducers({ a, b }) looks like this:
// const combined = combineReducers({ a, b })
function combined(state = {}, action) {
return {
a: a(state.a, action),
b: b(state.b, action)
}
}
If we create the store without a preloadedState, the combined reducer is going to initialize the state to {}. Therefore, state.a and state.b will be undefined by the time it calls a and b reducers. Both a and b reducers will receive undefined as their state arguments, and if they specify default state values, those will be returned. This is how the combined reducer returns a { a: 'lol', b: 'wat' } state object on the first invocation.
import { configureStore } from '@reduxjs/toolkit'
const store = configureStore({ reducer: combined })
console.log(store.getState()) // { a: 'lol', b: 'wat' }
Let's consider a different scenario:
import { configureStore } from '@reduxjs/toolkit'
const store = configureStore({
reducer: combined,
preloadedState: { a: 'horse' }
})
console.log(store.getState()) // { a: 'horse', b: 'wat' }
Now I specified a preloadedState. The state returned from the combined reducer combines the initial state I specified for the a reducer with the 'wat' default argument specified that b reducer chose itself.
Let's recall what the combined reducer does:
// const combined = combineReducers({ a, b })
function combined(state = {}, action) {
return {
a: a(state.a, action),
b: b(state.b, action)
}
}
In this case, state was specified so it didn't fall back to {}. It was an object with a field equal to 'horse', but without the b field. This is why the a reducer received 'horse' as its state and gladly returned it, but the b reducer received undefined as its state and thus returned its idea of the default state (in our example, 'wat'). This is how we get { a: 'horse', b: 'wat' } in return.
Recap
To sum this up, if you stick to Redux conventions and return the initial state from reducers when they're called with undefined as the state argument (the easiest way to implement this is to specify the state default argument value), you're going to have a nice useful behavior for combined reducers. They will prefer the corresponding value in the preloadedState object you pass when creating the store, but if you didn't pass any, or if the corresponding field is not set, the default state argument specified by the reducer is chosen instead. This approach works well because it provides both initialization and hydration of existing data, but lets individual reducers reset their state if their data was not preserved. Of course you can apply this pattern recursively, as you can use combineReducers() on many levels, or even compose reducers manually by calling reducers and giving them the relevant part of the state tree.
The initialState option of Redux Toolkit's createSlice works the same way: the generated slice reducer returns that value when it receives undefined, and a preloadedState passed to configureStore still takes precedence for that slice.