读取存储器在 Redux 还原器中的初始状态

可以通过两种方式设置 Redux 应用程序的初始状态:

  • 将其作为第二个参数传递给 createStore(文件链接)
  • 将它作为第一个参数传递给(子)还原器(文件链接)

如果将初始状态传递给存储,如何从存储读取该状态并将其作为 reducers 中的第一个参数?

49817 次浏览

In a nutshell: it's Redux the one who passes the initial state to the reducers, you don't need to do anything.

When you call createStore(reducer, [initialState]) you are letting Redux know what is the initial state to be passed to the reducer when the first action comes in.

The second option you mention, applies only in case you didn't pass an initial state when creating the store. i.e.

function todoApp(state = initialState, action)

state will only be initialised if there was no state passed by Redux

I hope this answers your request (which I understood as initializing reducers while passing intialState and returning that state)

This is how we do it (warning: copied from Typescript code).

The gist of it is the if(!state) test in the mainReducer(factory) function

function getInitialState(): MainState {


return {
prop1:                 'value1',
prop1:                 'value2',
...
}
}






const reducer = combineReducers(
{
main:     mainReducer( getInitialState() ),
...
}
)






const mainReducer = ( initialState: MainState ): Reducer => {


return ( state: MainState, action: Action ): MainState => {


if ( !state ) {
return initialState
}


console.log( 'Main reducer action: ', action )


switch ( action.type ) {
....
}
}
}

how do you read that state from the store and make it the first argument in your reducers?

combineReducers() do the job for you. The first way to write it is not really helpfull :

const rootReducer = combineReducers({ todos, users })

But the other one, that is equivalent is more clear :

function rootReducer(state, action) {
todos: todos(state.todos, action),
users: users(state.users, action)
}

TL;DR

Without combineReducers() or similar manual code, initialState always wins over state = ... in the reducer because the state passed to the reducer is initialState and is not undefined, so the ES6 argument syntax doesn't get applied in this case.

With combineReducers() the behavior is more nuanced. Those reducers whose state is specified in initialState 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, initialState 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.

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 { createStore } from 'redux';
let store = createStore(counter);
console.log(store.getState()); // 0

The initial state is zero. Why? Because the second argument to createStore 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. undefined1 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 { createStore } from 'redux';
let store = createStore(counter, 42);
console.log(store.getState()); // 42

Why is it 42, and not 0, this time? Because createStore was called with 42 as the second argument. This argument becomes the state passed to your reducer along with the dummy action. 00 The state is 42, and 42 is returned from the reducer.


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 call createStore without the initialState, it's going to initialize the state to {}. Therefore, state.a and state.b will be undefined by the time it calls a and b reducers. initialState5 This is how the combined reducer returns a initialState4 state object on the first invocation.

import { createStore } from 'redux';
let store = createStore(combined);
console.log(store.getState()); // { a: 'lol', b: 'wat' }

Let's consider a different scenario:

import { createStore } from 'redux';
let store = createStore(combined, { a: 'horse' });
console.log(store.getState()); // { a: 'horse', b: 'wat' }

Now I specified the initialState as the argument to createStore(). 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 {}4 of the default state (in our example, {}2). This is how we get {}3 in return.


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 ES6 default argument value), you're going to have a nice useful behavior for combined reducers. They will prefer the corresponding value in the ABC3 object you pass to the ABC4 function, 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.