Przeczytaj stan początkowy sklepu w Redux Reducer

85

Stan początkowy w aplikacji Redux można ustawić na dwa sposoby:

Jeśli przekazujesz stan początkowy do swojego sklepu, jak odczytać ten stan ze sklepu i uczynić go pierwszym argumentem w twoich reduktorach?

cantera
źródło

Odpowiedzi:

185

TL; DR

Bez combineReducers()lub podobnego kodu ręcznego initialStatezawsze wygrywa state = ...w reduktorze, ponieważ stateprzekazany do reduktora jest initialState i nie jest undefined , więc składnia argumentu ES6 nie jest stosowana w tym przypadku.

Dzięki combineReducers()zachowaniu jest bardziej zniuansowany. Te reduktory, których stan jest określony w initialState, otrzymają to state. Inne reduktory otrzymają undefined iz tego powodu powrócą do state = ...domyślnego argumentu, który określą.

Generalnie initialStatewygrywa stan określony przez reduktor. Pozwala to reduktorom określić początkowe dane, które mają dla nich sens jako domyślne argumenty, ale także umożliwia ładowanie istniejących danych (w całości lub częściowo) podczas nawadniania sklepu z trwałej pamięci masowej lub serwera.

Najpierw rozważmy przypadek, w którym masz pojedynczy reduktor.
Powiedz, że nie używasz combineReducers().

Wtedy twój reduktor może wyglądać tak:

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

Teraz powiedzmy, że tworzysz z nim sklep.

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

Stan początkowy wynosi zero. Czemu? Ponieważ drugim argumentem createStorebyło undefined. To jest stateprzekazane do twojego reduktora za pierwszym razem. Podczas inicjalizacji Redux wysyła „fikcyjną” akcję, aby wypełnić stan. Więc twój counterreduktor został wywołany z staterówną undefined. Dokładnie tak jest, gdy „aktywuje” domyślny argument. Dlatego statejest teraz 0zgodnie z statewartością domyślną ( state = 0). Ten stan ( 0) zostanie zwrócony.

Rozważmy inny scenariusz:

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

Dlaczego tym razem tak jest 42, a nie 0? Ponieważ createStorezostał wywołany 42jako drugi argument. Ten argument jest stateprzekazywany do twojego reduktora wraz z fikcyjną akcją. Tym razem statenie jest niezdefiniowana (jest 42!), Więc domyślna składnia argumentów ES6 nie ma znaczenia. stateJest 42, i 42jest zwrócony od reduktora.


Rozważmy teraz przypadek, w którym używasz combineReducers().
Masz dwa reduktory:

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

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

Reduktor generowany przez combineReducers({ a, b })wygląda następująco:

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

Jeśli zadzwonimy createStorebez tego initialState, zainicjuje stateto {}. Dlatego state.ai state.bbędzie undefineddo czasu, gdy wzywa ai bredukuje. Oba ai breduktory otrzymają undefinedjako swoje state argumenty, a jeśli określą statewartości domyślne , zostaną one zwrócone. W ten sposób połączony reduktor zwraca { a: 'lol', b: 'wat' }obiekt stanu przy pierwszym wywołaniu.

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

Rozważmy inny scenariusz:

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

Teraz określiłem initialStatejako argument do createStore(). Stan zwrócony z połączonego reduktora łączy stan początkowy określony dla areduktora z 'wat'domyślnym argumentem określonym, który bsam wybrał reduktor.

Przypomnijmy, co robi kombinowany reduktor:

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

W tym przypadku statezostał określony, więc nie powrócił do {}. Był to obiekt o apolu równym 'horse', ale bez bpola. Dlatego areduktor otrzymał 'horse'jako swój statei chętnie go zwrócił, ale breduktor otrzymał undefinedjako swój statei w ten sposób zwrócił swoją ideę domyślną state(w naszym przykładzie 'wat'). Tak otrzymujemy { a: 'horse', b: 'wat' }w zamian.


Podsumowując tę górę, jeśli trzymać się konwencji Redux i przywrócić stan początkowy z reduktorów, kiedy nazywa się je ze undefinedjako stateargumentu (najprostszym sposobem realizacji tego celu jest określenie statewartości domyślne argumentów ES6), będziesz mieć ładne użyteczne zachowanie dla połączonych reduktorów. Będą preferować odpowiednią wartość w initialStateobiekcie, który przekazujesz do createStore()funkcji, ale jeśli nie przekazałeś żadnej lub jeśli odpowiednie pole nie jest ustawione, statezamiast tego wybierany jest domyślny argument określony przez reduktor.To podejście działa dobrze, ponieważ zapewnia zarówno inicjalizację, jak i hydratację istniejących danych, ale umożliwia poszczególnym reduktorom resetowanie ich stanu, jeśli ich dane nie zostały zachowane. Oczywiście możesz zastosować ten wzorzec rekurencyjnie, ponieważ możesz go używać combineReducers()na wielu poziomach, a nawet ręcznie tworzyć redukcje, wywołując redukcje i dając im odpowiednią część drzewa stanu.

Dan Abramov
źródło
3
Dzięki za szczegóły - dokładnie to, czego szukałem. Elastyczność twojego API jest tutaj genialna. Częścią, której nie widziałem, jest to, że „dziecięcy” reduktor zrozumie, który element stanu początkowego należy do niego na podstawie użytego klucza combineReducers. Jeszcze raz bardzo dziękuję.
cantera
Dzięki za tę odpowiedź, Dan. Jeśli poprawnie przetrawiam twoją odpowiedź, to mówisz, że najlepiej byłoby ustawić stan początkowy poprzez rodzaj działania reduktora? Na przykład GET_INITIAL_STATE i wywołanie wysyłki do tego typu akcji w, powiedzmy, wywołaniu zwrotnym componentDidMount?
Con Antonakos
@ConAntonakos Nie, nie mam tego na myśli. Mam na myśli wybór stanu początkowego dokładnie tam, gdzie definiujemy reduktor np . function counter(state = 0, action)Lub function visibleIds(state = [], action).
Dan Abramov
1
@DanAbramov: używając drugiego argumentu w createstore (), jak mogę ponownie nawodnić stan po przeładowaniu strony SPA z ostatnim zapisanym stanem w redux zamiast wartości początkowych. Myślę, że pytam, jak mogę buforować cały sklep i zachować go przy ponownym załadowaniu i przekazać do funkcji createstore.
jasan
1
@jasan: użyj localStorage. egghead.io/lessons/ ...
Dan Abramov
5

Krótko mówiąc: to Redux przekazuje stan początkowy reduktorom, nie musisz nic robić.

Kiedy dzwonisz createStore(reducer, [initialState]), informujesz Redux, jaki jest stan początkowy, który ma zostać przekazany do reduktora, gdy nadejdzie pierwsza akcja.

Druga opcja, o której wspomniałeś, ma zastosowanie tylko w przypadku, gdy nie przeszedłeś stanu początkowego podczas tworzenia sklepu. to znaczy

function todoApp(state = initialState, action)

stan zostanie zainicjowany tylko wtedy, gdy żaden stan nie został przekazany przez Redux

Emilio Rodriguez
źródło
1
Dziękuję za odpowiedź - w przypadku składu reduktora, jeśli zastosowałeś stan początkowy do sklepu, w jaki sposób możesz powiedzieć dziecku / reduktorowi podrzędnemu, którą część drzewa stanu początkowego posiada? Na przykład, jeśli masz menuState, jak mam powiedzieć reduktorowi menu, aby odczytał wartość state.menuze sklepu i użył tego jako stanu początkowego?
cantera
Dzięki, ale moje pytanie musi być niejasne. Przed edycją poczekam, aż inni odpowiedzą.
cantera
1

jak odczytać ten stan ze sklepu i uczynić go pierwszym argumentem w twoich reduktorach?

connectReducers () wykonują pracę za Ciebie. Pierwszy sposób, aby to napisać, nie jest zbyt pomocny:

const rootReducer = combineReducers({ todos, users })

Ale druga, równoważna, jest bardziej jasna:

function rootReducer(state, action) {
   todos: todos(state.todos, action),
   users: users(state.users, action)
}
Nicolas
źródło
0

Mam nadzieję, że to odpowiedź na Twoją prośbę (którą rozumiałem jako inicjowanie reduktorów podczas przechodzenia przez stan intialState i zwracania tego stanu)

Tak to robimy (uwaga: skopiowano z kodu Typescript).

Istotą tego jest if(!state)test w funkcji mainReducer (fabrycznej)

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
źródło