Dynamika zmian: architektura Redux

Założenia

Koncepcja sterowania zmianami poprzez stany w bibliotece Redux została zastosowana do całej aplikacji. Trzeba jednak odróżnić stan (state) Reduxa i stan (state) w komponentach. W pierwszym przypadku state to wszystkie informacje o stanie aplikacji: np. jakie dane zostały już pobrane z serwera, jaka jest zawartość formularza, jaki użytkownik jest zalogowany, jakie okienko jest otwarte. Stan komponentu jest bardzo „lokalny” (dotyczy tylko komponentu)1. Redux służy do jednolitego przetwarzania stanu całej aplikacji.

Obiekt zarządzający stanem aplikacji jest nazywany store. Składa się on z aktualnego stanu (state - obiekt JavaScript) oraz funkcji reducer, która na podstawie obecnego stanu i wykonanej akcji wylicza następny.

Jednokierunkowy przepływ danych

Nie wnikając w szczegóły – poza props i state w każdym obiekcie jest context który działa tak jak props, ale udostępnia dane dla potomków. Dzięki temu jest możliwe połączenie między obiektami. Instrukcja bind wspomniana wyżej (this.onClick = this.onClick.bind(this); ) umożliwia właśnie zmianę kontekstu przy wywołaniu onClick

.

Implementacje

Zacznijmy od najbardziej banalnego przykładu - definicji store z jedną zmienną (licznik) i jedną akcją:

import {createStore } from 'redux';

function stanPoczatkowy() {
  return {
    licznik : 0
  }
}

const naszReducer = (state, action) => {
  if (action.type === ACTION_KLIKNIECIE) {
    let nowyLicznik=state.licznik+1;
    return( {licznik:nowyLicznik} );
  } else return state;
}

const ACTION_KLIKNIECIE = 'ACTION_KLIKNIECIE';

export function klikniecie() {
  return {
    type: ACTION_KLIKNIECIE
  }
}

export const store = createStore(
  naszReducer,
  stanPoczatkowy()
)

Objaśnienie:

  • stanPoczatkowy- prosta funkcja zwracająca stan początkowy (tu tylko jedno pole: licznik)
  • naszReducer - zdefiniowanie funkcji reducera.- gdy klikniecie - zwiększa licznik; w p.p. zwraca stan;
  • klikniecie - producent akcji ACTION_KLIKNIECIE (tu: tylko typ akcji);
  • store = createStore() - utworzenie sore dla danego reducera i stanu początkowego

Użycie store polega na tym, że stan i zdarzenia zostają "wstrzyknięte" do własności obiektu. Dokonuje się to przy pomocy funkcji connect z biblioteki react-redux. Funkcja ta ma dwa parametry - funkcję zwracającą obiekt stanu (zwyczajowo tą funkcję nazywamy mapStatetoProps) oraz funkcję mapującą akcje (o zwyczajowej nazwie mapDispatchToProps):

const mapStateToProps = stanMagazynu => {
  return {
    stan_redux : stanMagazynu
  }
}
const mapDispatchToProps = dispatch => bindActionCreators(
  {  klikniecie },
  dispatch
)
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(App)

Stan jest wstrzykiwany pod nazwą stan_redux (dowolna). Czyli można się do niego odwołać poprzez this.props.stan_redux.(funkcja z biblioteki redux). Akcje są wstrzykiwane pod nazwami kreatorów podanych w parametrze bindActionCreators(). Czyli w nszym przykładzie wśród własnosci pojawi się funkcja klikniecie (this.props.klikniecie).

Składnia ostatniego polecenia zapewnia, że eksportowany nie jest oryginalny komponent App, ale komponent ze wstrzykniętymi akcjami i stanem.

Pełna implementacja modułu App.js:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators} from 'redux';
import { klikniecie } from './magazyn';

class App extends Component {

  render() {
    return (
      <div className="App">
      <button onClick={this.props.klikniecie}>
        Licznik = {this.props.stan_redux.licznik}
      </button>
      </div>
    );
  }
}

//export default App;
const mapStateToProps = stanMagazynu => {
  return {
    stan_redux : stanMagazynu
  }
}
const mapDispatchToProps = dispatch => bindActionCreators(
  {  klikniecie },
  dispatch
)
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(App)

Ostatnią rzeczą jaką musimy zrobić, jest dodanie providera do renderowania przez ReactDOM:

import {store} from './magazyn';
import {Provider} from 'react-redux';

ReactDOM.render(
  <Provider store = {store }>
  <App />
  </Provider>
  , document.getElementById('root'));

Zobacz cały przykład: [021]

Uwagi uzupełniające

W definicji store korzysta się zazwyczaj z nowoczesnych konstrukcji Javascript:

  1. Przy tworzeniu kopii stanu (nowy stan) korzystamy ze "spread operator" (dodanie nie zmienionych elementów) i/lub "object assign"(zob.https://googlechrome.github.io/samples/object-assign-es6/); użycie {} jako pierwszego parametru zapewnia, że dostaniemy klon obiektu (state) - ob przykład niżej.
  2. Zamiast podawać stan początkowy w parametrze createStore, można go zdefiniować jako domyślną wartość parametru reducera. Przy pierwszym wykonaniu stan będzie nieokreślony, więc zostanie wstawiony stan początowy (taka hakerska sztuczka).
  3. Zamiast if najlepiej od razu stosować switch- nie wiadomo ile jeszcze dodamy akcji.

Nasz store po uwzględnieniu tych uwag będzie wyglądał następująco:

Przykład definicji store:

import {createStore } from 'redux';

const ACTION_KLIKNIECIE = 'ACTION_KLIKNIECIE';

const naszReducer = (state = { licznik: 0, }, action) => {
  switch (action.type) {
    case ACTION_KLIKNIECIE: 
      let nowyLicznik=state.licznik+1;
      return Object.assign({}, state, {
         licznik : nowyLicznik,
        });
    default:
        return state;
  }
}

export function klikniecie() {
  return {
    type: ACTION_KLIKNIECIE
  }
}

export const store = createStore( naszReducer, );

Zobacz przykład: [022]

Możliwą alternatywą do wstrzykiwania akcji, jest stworzenie obiektu akcji i odwoływanie się do niego, zamiast do elementu własności (props).

export const Akcje = {
  klikniecie (): void {
    store.dispatch({ type: ACTION_KLIKNIECIE });
  }
}

Na pewno jest to dużo bardziej przejrzyste - widzimy jawne odwołanie do store.dispatch. Ponieważ jednak nie jest to rozwiązanie zalecane - można się liczyć z różnymi utrudnieniami.

Więcej prakycznych przykładów:https://github.com/galicea/eduprog/tree/master/web/react/redux

Przypis do rozdziału:

1Może to np. być aktualne powiększenie na wyświetlanej mapie itd

2Zobacz:http://redux.js.org/docs/basics/Actions.html#action-creators

Zalety / wady

Redux choć wydaje się koncepcyjnie trudny, a jego powiązanie z React wymaga kilku czynności. Znacznie ułatwia on jednak życie. Nie ma jednak potrzeby, aby go stosować, gdy nie wystepuje konieczność przekazywania stanu między komponentami na przykład pojedynczy formularz).