Leer el estado inicial de la tienda en Redux Reducer

85

El estado inicial en una aplicación Redux se puede configurar de dos maneras:

Si pasa el estado inicial a su tienda, ¿cómo lee ese estado de la tienda y lo convierte en el primer argumento en sus reductores?

cantera
fuente

Respuestas:

185

TL; DR

Sin combineReducers()código manual o similar, initialStatesiempre gana state = ...en el reductor porque lo que se statepasa al reductor es initialState y no es undefined , por lo que la sintaxis del argumento ES6 no se aplica en este caso.

Con combineReducers()el comportamiento es más matizado. Los reductores cuyo estado se especifica en initialStaterecibirán eso state. Otros reductores recibirán undefined y, debido a eso , volverán al state = ...argumento predeterminado que especifican.

En general, initialStategana el estado especificado por el reductor. Esto permite que los reductores especifiquen datos iniciales que tengan sentido para ellos como argumentos predeterminados, pero también permite cargar datos existentes (total o parcialmente) cuando está hidratando la tienda desde algún almacenamiento persistente o el servidor.

Primero, consideremos un caso en el que tiene un solo reductor.
Di que no usas combineReducers().

Entonces su reductor podría verse así:

function counter(state = 0, action) {
  switch (action.type) {
  case 'INCREMENT': return state + 1;
  case 'DECREMENT': return state - 1;
  default: return state;
  }
}

Ahora digamos que crea una tienda con él.

import { createStore } from 'redux';
let store = createStore(counter);
console.log(store.getState()); // 0

El estado inicial es cero. ¿Por qué? Porque el segundo argumento de createStorefue undefined. Este es el statepasado a su reductor la primera vez. Cuando Redux se inicializa, envía una acción "ficticia" para completar el estado. Entonces su counterreductor fue llamado stateigual a undefined. Este es exactamente el caso que "activa" el argumento predeterminado. Por lo tanto, stateahora tiene 0el statevalor predeterminado ( state = 0). Se 0devolverá este estado ( ).

Consideremos un escenario diferente:

import { createStore } from 'redux';
let store = createStore(counter, 42);
console.log(store.getState()); // 42

¿Por qué es 42y no 0esta vez? Porque createStorefue llamado con 42como segundo argumento. Este argumento se statepasa a su reductor junto con la acción ficticia. Esta vez, stateno está indefinido (¡lo está 42!), Por lo que la sintaxis del argumento predeterminado de ES6 no tiene ningún efecto. El statees 42y 42se devuelve del reductor.


Ahora consideremos un caso en el que usa combineReducers().
Tienes dos reductores:

function a(state = 'lol', action) {
  return state;
}

function b(state = 'wat', action) {
  return state;
}

El reductor generado por combineReducers({ a, b })tiene este aspecto:

// const combined = combineReducers({ a, b })
function combined(state = {}, action) {
  return {
    a: a(state.a, action),
    b: b(state.b, action)
  };
}

Si llamamos createStoresin el initialState, se inicializará el statea {}. Por tanto, state.ay state.bserá undefineden el momento en que llame ay breduzca. Tanto ay breductores recibirán undefinedcomo sus state argumentos, y si especifican por defecto statelos valores, los que serán devueltos. Así es como el reductor combinado devuelve un { a: 'lol', b: 'wat' }objeto de estado en la primera invocación.

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

Consideremos un escenario diferente:

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

Ahora especifiqué initialStatecomo argumento createStore(). El estado devuelto por el reductor combinado combina el estado inicial que especifiqué para el areductor con el 'wat'argumento predeterminado especificado que el breductor eligió a sí mismo.

Recordemos lo que hace el reductor combinado:

// const combined = combineReducers({ a, b })
function combined(state = {}, action) {
  return {
    a: a(state.a, action),
    b: b(state.b, action)
  };
}

En este caso, statese especificó para no volver a {}. Era un objeto con acampo igual a 'horse', pero sin bcampo. Es por eso que el areductor lo recibió 'horse'como statesuyo y lo devolvió con gusto, pero el breductor lo recibió undefinedcomo suyo statey, por lo tanto, devolvió su idea del valor predeterminado state(en nuestro ejemplo, 'wat'). Así es como recibimos { a: 'horse', b: 'wat' }a cambio.


En resumen, si se apega a las convenciones de Redux y devuelve el estado inicial de los reductores cuando se llaman con undefinedel stateargumento (la forma más fácil de implementar esto es especificar el statevalor del argumento predeterminado de ES6), tendrá un buen comportamiento útil para reductores combinados. Preferirán el valor correspondiente en el initialStateobjeto que pasa a la createStore()función, pero si no pasó ninguno, o si el campo correspondiente no está configurado, statese elige el argumento predeterminado especificado por el reductor.Este enfoque funciona bien porque proporciona tanto la inicialización como la hidratación de los datos existentes, pero permite que los reductores individuales restablezcan su estado si sus datos no se conservaron. Por supuesto, puede aplicar este patrón de forma recursiva, como puede utilizar combineReducers()en muchos niveles, o incluso componer reductores manualmente llamando a los reductores y dándoles la parte relevante del árbol de estado.

Dan Abramov
fuente
3
Gracias por el gran detalle, exactamente lo que estaba buscando. La flexibilidad de su API aquí es brillante. La parte que no estaba viendo es que el reductor "secundario" entenderá a qué parte del estado inicial le pertenece según la clave utilizada en combineReducers. Muchisímas gracias de nuevo.
cantera
Gracias por esta respuesta, Dan. Si estoy asimilando su respuesta correctamente, ¿está diciendo que sería mejor establecer un estado inicial a través del tipo de acción de un reductor? Por ejemplo, GET_INITIAL_STATE y llamar a un despacho a ese tipo de acción, digamos, ¿una devolución de llamada componentDidMount?
Con Antonakos
@ConAntonakos No, no me refiero a esto. Me refiero a elegir el estado inicial justo donde se define el reductor, por ejemplo, function counter(state = 0, action)o function visibleIds(state = [], action).
Dan Abramov
1
@DanAbramov: usando el segundo argumento en createtore (), ¿cómo puedo rehidratar el estado al volver a cargar la página SPA con el último estado almacenado en redux en lugar de los valores iniciales? Supongo que estoy preguntando cómo puedo almacenar en caché toda la tienda y conservarla en la recarga y pasarla a la función createtore.
jasan
1
@jasan: usa localStorage. egghead.io/lessons/…
Dan Abramov
5

En pocas palabras: es Redux el que pasa el estado inicial a los reductores, no necesitas hacer nada.

Cuando llama, createStore(reducer, [initialState])le está haciendo saber a Redux cuál es el estado inicial que se pasará al reductor cuando entre la primera acción.

La segunda opción que mencionas, se aplica solo en caso de que no hayas pasado un estado inicial al crear la tienda. es decir

function todoApp(state = initialState, action)

el estado solo se inicializará si no hubo ningún estado pasado por Redux

Emilio Rodríguez
fuente
1
Gracias por responder: en el caso de la composición del reductor, si ha aplicado el estado inicial a la tienda, ¿cómo le dice al niño / sub reductor qué parte del árbol del estado inicial es propietario? Por ejemplo, si tiene un menuState, ¿cómo le digo al reductor de menú que lea el valor de state.menude la tienda y lo use como su estado inicial?
cantera
Gracias, pero mi pregunta debe ser confusa. Esperaré a ver si otros responden antes de editar.
cantera
1

¿Cómo lee ese estado de la tienda y lo convierte en el primer argumento en sus reductores?

combineReducers () hace el trabajo por usted. La primera forma de escribirlo no es realmente útil:

const rootReducer = combineReducers({ todos, users })

Pero el otro, que es equivalente, es más claro:

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

Espero que esto responda a su solicitud (que entendí como inicializar reductores al pasar intialState y devolver ese estado)

Así es como lo hacemos (advertencia: copiado del código Typecript).

La esencia es la if(!state)prueba en la función mainReducer (fábrica)

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 ) {
            ....
        }
    }
}
Bruno Grieder
fuente