Leggi lo stato iniziale del negozio in Redux Reducer


85

Lo stato iniziale in un'app Redux può essere impostato in due modi:

Se passi lo stato iniziale al tuo negozio, come puoi leggere quello stato dal negozio e renderlo il primo argomento nei tuoi riduttori?

Risposte:


185

TL; DR

Senza combineReducers()o simile codice manuale, initialStatevince sempre state = ...nel riduttore perché il statepassato al riduttore è initialState e non lo è undefined , quindi la sintassi dell'argomento ES6 non viene applicata in questo caso.

Con combineReducers()il comportamento è più sfumato. Quei riduttori il cui stato è specificato in initialStatelo riceveranno state. Gli altri riduttori riceveranno undefined e per questo motivo torneranno state = ...all'argomento predefinito specificato.

In generale, initialStatevince sullo stato specificato dal riduttore. Ciò consente ai riduttori di specificare i dati iniziali che hanno senso per loro come argomenti predefiniti, ma consente anche di caricare i dati esistenti (completamente o parzialmente) quando si idrata l'archivio da una memoria persistente o dal server.

Per prima cosa consideriamo un caso in cui hai un solo riduttore.
Dì che non usi combineReducers().

Quindi il tuo riduttore potrebbe assomigliare a questo:

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

Supponiamo ora che crei un negozio con esso.

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

Lo stato iniziale è zero. Perché? Perché il secondo argomento createStoreera undefined. Questa è statela prima volta che passi al tuo riduttore. Quando Redux si inizializza, invia un'azione "fittizia" per riempire lo stato. Quindi il tuo counterriduttore è stato chiamato con stateuguale a undefined. Questo è esattamente il caso in cui "attiva" l'argomento predefinito. Pertanto, stateè ora 0come per il statevalore predefinito ( state = 0). Questo stato ( 0) verrà restituito.

Consideriamo uno scenario diverso:

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

Perché è 42, e non 0, questa volta? Perché è createStorestato chiamato con 42come secondo argomento. Questo argomento diventa il statepassato al tuo riduttore insieme all'azione fittizia. Questa volta statenon è undefined (lo è 42!), Quindi la sintassi degli argomenti predefinita di ES6 non ha effetto. Il stateè 42e 42viene restituito dal riduttore.


Consideriamo ora un caso in cui utilizzi combineReducers().
Hai due riduttori:

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

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

Il riduttore generato da si combineReducers({ a, b })presenta così:

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

Se chiamiamo createStoresenza initialState, inizializzerà il file statea {}. Pertanto, state.ae state.bsarà undefinedper il momento in cui chiama ae briduzioni. Sia ae briduttori riceveranno undefinedcome i loro state argomenti, e se specificano predefiniti statevalori, quelli saranno restituiti. Questo è il modo in cui il riduttore combinato restituisce un { a: 'lol', b: 'wat' }oggetto di stato alla prima chiamata.

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

Consideriamo uno scenario diverso:

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

Ora ho specificato initialStatecome argomento per createStore(). Lo stato restituito dal riduttore combinato combina lo stato iniziale specificato per il ariduttore con l' 'wat'argomento predefinito specificato dal briduttore stesso.

Ricordiamo cosa fa il riduttore combinato:

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

In questo caso, è statestato specificato in modo da non ricadere su {}. Era un oggetto con acampo uguale a 'horse', ma senza bcampo. Questo è il motivo per cui il ariduttore ha ricevuto 'horse'come suo statee lo ha restituito volentieri, ma il briduttore ha ricevuto undefinedcome suo statee quindi ha restituito la sua idea di default state(nel nostro esempio 'wat'). Ecco come otteniamo { a: 'horse', b: 'wat' }in cambio.


Per riassumere, se ti attieni alle convenzioni Redux e restituisci lo stato iniziale dai riduttori quando vengono chiamati con undefinedcome stateargomento (il modo più semplice per implementarlo è specificare il statevalore dell'argomento predefinito di ES6), avrai un bel comportamento utile per i riduttori combinati. Preferiranno il valore corrispondente initialStatenell'oggetto che passi alla createStore()funzione, ma se non ne hai passato nessuno, o se il campo corrispondente non è impostato, stateviene invece scelto l' argomento predefinito specificato dal riduttore.Questo approccio funziona bene perché fornisce sia l'inizializzazione che l'idratazione dei dati esistenti, ma consente ai singoli riduttori di ripristinare il proprio stato se i dati non sono stati conservati. Ovviamente puoi applicare questo modello in modo ricorsivo, poiché puoi usarlo combineReducers()su molti livelli, o anche comporre manualmente i riduttori chiamando i riduttori e dando loro la parte rilevante dell'albero degli stati.


3
Grazie per l'ottimo dettaglio, esattamente quello che stavo cercando. La flessibilità della tua API qui è eccezionale. La parte che non vedevo è che il riduttore "figlio" capirà quale parte dello stato iniziale gli appartiene in base alla chiave utilizzata in combineReducers. Grazie mille ancora.
cantera

Grazie per questa risposta, Dan. Se digerisco correttamente la tua risposta, stai dicendo che sarebbe meglio impostare uno stato iniziale tramite il tipo di azione di un riduttore? Ad esempio, GET_INITIAL_STATE e chiamare un invio a quel tipo di azione in, diciamo, una richiamata componentDidMount?
Con Antonakos

@ConAntonakos No, non intendo questo. Intendo scegliere lo stato iniziale proprio dove si definisce il riduttore, ad esempio function counter(state = 0, action)o function visibleIds(state = [], action).
Dan Abramov

1
@DanAbramov: utilizzando il secondo argomento in createstore (), come posso reidratare lo stato al ricaricamento della pagina SPA con l'ultimo stato memorizzato in redux invece dei valori iniziali. Immagino di chiedermi come posso memorizzare nella cache l'intero negozio e conservarlo durante la ricarica e passarlo alla funzione createstore.
jasan

1
@jasan: usa localStorage. egghead.io/lessons/…
Dan Abramov

5

In poche parole: è Redux quello che passa lo stato iniziale ai riduttori, non devi fare nulla.

Quando chiami, createStore(reducer, [initialState])stai facendo sapere a Redux qual è lo stato iniziale da passare al riduttore quando arriva la prima azione.

La seconda opzione che hai menzionato si applica solo nel caso in cui non hai superato uno stato iniziale durante la creazione del negozio. cioè

function todoApp(state = initialState, action)

state verrà inizializzato solo se non ci sono stati stati passati da Redux


1
Grazie per la risposta: nel caso della composizione del riduttore, se hai applicato lo stato iniziale al negozio, come fai a dire al riduttore secondario / secondario quale parte dell'albero dello stato iniziale possiede? Ad esempio, se hai un menuState, come faccio a dire al riduttore di menu di leggere il valore di state.menudal negozio e di usarlo come stato iniziale?
cantera

Grazie, ma la mia domanda non deve essere chiara. Aspetterò di vedere se altri rispondono prima di modificare.
cantera

1

come leggi quello stato dal negozio e renderlo il primo argomento nei tuoi riduttori?

combinareReducers () fa il lavoro per te. Il primo modo per scriverlo non è molto utile:

const rootReducer = combineReducers({ todos, users })

Ma l'altro, che è equivalente è più chiaro:

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

0

Spero che questo risponda alla tua richiesta (che ho inteso come inizializzazione di riduttori mentre si passa intialState e si restituisce quello stato)

Ecco come lo facciamo (attenzione: copiato dal codice Typescript).

L'essenza è il if(!state)test nella funzione mainReducer (factory)

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 ) {
            ....
        }
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.