Redux. Wybrane problemy.

Bezpośrednie wywołanie dispatch()

Wykonanie połączenia Redux z React powoduje wstrzyknięcie do własności procedury dispatch, której możemy użyć do wywołania akcji (nawet jeśli nie mamy "fabryki akcji"). Zobaczmy to na przykładzie przycisku który zwiększa nam licznik (zapisany w store). Załóżmy, że między naciśnięciem przycisku a wykonaniem akcji (zwiększenie licznika) musi upłynąć trochę czasu i przez ten czas dezaktywujemy przycisk (pole "zwiekszamy").

Zróbmy zatem dwie akcje INCREMENT_REQUEST (rozpoczęscie / dezaktywacja) i INCREMENT:

// akcje
export const INCREMENT_REQUEST = 'INCREMENT_REQUEST';
export const INCREMENT = 'INCREMENT';


export const stanPoczatkowy = {
  licznik : 0,
  zwiekszany: false
}

export default /*const reducer = */ (state = stanPoczatkowy, action) => {
  switch (action.type) {
    case INCREMENT_REQUEST:
      return { ...state, zwiekszany: true }
    case INCREMENT:
      return { ...state,
               licznik: state.licznik+1,
               zwiekszany: false }
    default:
      return state
  }

}

Ponieważ robimy export default - nie musimy deklarować "const reducer". Sam store założymy bez fabryki akcji:

import reducerLicznik from './licznik';
import {createStore } from 'redux';

export default createStore(
  reducerLicznik,
  undefined 
);

Definiujemy render i zwiększanie licznika (do symulacji opóźnienia wykorzystaliśmy funkcję setTimeout Javascript). Zakładamy, że stan jest wstrzyknięty w pole licznik.

import React, { Component } from 'react';

import {increment, INCREMENT_REQUEST, INCREMENT } from './licznik';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';

class App extends Component {

  handleClick = () => {
    console.log('increment: start');
    this.props.dispatch( {
      type: INCREMENT_REQUEST
    });
    console.log('increment: delay');
    setTimeout(() => {  // 2 sekundy opóźnienia
      console.log('increment: execute');
      this.props.dispatch( {
        type: INCREMENT
      });
    }, 2000);
  }

  render() {
    return (
      <div>
          <button onClick={ this.handleClick }
             disabled={this.props.licznik.zwiekszany }
          >
          Zwiększ
          </button>
          <p>
            Stan = {this.props.licznik.licznik}
          </p>
      </div>
    );
  }
}

Do komponentu App mapujemy tylko stan (chyba, że bezpośrednie wywołanie dispatch to tylko jedna z opcji):

const mapStateToProps = state => (
  { licznik: state    }
)

 export default connect(mapStateToProps)(App);

Zobacz przykład [027]

Wiele reduktorów

Reduktor możemy w złożonych aplikacjach składać z wielu prostszych reduktorów. Załóżmy, że nasz licznik z poprzedniego przykładu to tylko jeden z wielu reduktorów. Jakich modyfikacji musimy dokonać?

1) Zmieniamy nazwy stałych określających akcje. Nie jest to obowiązkowe, ale w ten sposób unikamy możliwych powtórzeń:

export const INCREMENT_REQUEST = 'licznik/INCREMENT_REQUEST';
export const INCREMENT = 'licznik/INCREMENT';

2) Tworzymy reduktor złożony. Do każdego ze składników będziemy odwoływać się po jego nazwie (poniżej reduktor z modułu licznik uzyskuje nazwę rliczcnik). Zauważ, że w funkcji createStore używamy reduktora złożonego!:

import reducerLicznik  from './licznik';
import {createStore, combineReducers } from 'redux';

const reducer = combineReducers({
   rlicznik: reducerLicznik,
   // .... ewentualne dodatkowe reduktory
 });

export default createStore(
  reducer, // złożony reducer!
  undefined //stanPoczatkowy niepotrzebny - jest domyślna wartość parametru
);

3) Zmieniamy odwołania do wstrzykniętego stanu dodając człon .rlicznik.:

  render() {
    return (
      <div>
          <button onClick={ this.handleClick }
             disabled={this.props.licznik.rlicznik.zwiekszany }
          >
          Zwiększ
          </button>
          <p>
            Stan = {this.props.licznik.rlicznik.licznik}
          </p>
      </div>
    );
  }
}

Jako alternatywę (jeśli w komponencie używamy tylko jednego z reduktorów) - można zmienić procedurę definiującą wstrzykiwanie:

const mapStateToProps = state => { 
  return { licznik: state.rlicznik    }
}

Zobacz przykład: [028]

Middleware

W procesie przesyłania danych między store Reduxa a komponentami możne następować przetwarzanie tych danych. Służą do tego moduły dodatkowe middleware. Pokażemy to na przykładzie dwóch takich modułów (trzeba je doinstalować przez yarn add ...):

  • redux-thunk - pozwala na wysyłanie akcji w postaci funkcji (a nie struktur definiujących); dopiero te funkcje generują klasyczne akcje, przekazując je poprzez dispatch (zob. niżej, inny opis: https://typeofweb.com/2018/05/07/asynchronicznosc-w-redux-redux-thunk/\;)
  • redux-logger - reduktor to idealne miejsce na logowanie (zapis do loga) wszystkiego co dzieje się w aplikacji; ten dodatek służy właśnie do tego.

Najpier instalujemy (yarn add) dodatki i uwzględniamy je w definicji store:

import reducerLicznik from './licznik';
import {createStore, applyMiddleware} from 'redux';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
import { combineReducers } from 'redux';

const middleware = [thunk, logger];

const reducer = combineReducers({
   rlicznik: reducerLicznik,
   // ....
 });

export default createStore(
  reducer,
  undefined,
  applyMiddleware(...middleware)
);

Teraz możemy przenieść obsługę funkcji zwiększania licznika (zob. poprzedni przykład) - do modułu licznik.js. Funkcja dla redux-thunk - musi zwrócić funkcję obsługującą dispatcher (rozsyłanie) akcji:

export const increment = () => {
  return dispatch => {
      dispatch( {
        type: INCREMENT_REQUEST
      });
      return setTimeout(
        () => {
          dispatch( {
            type: INCREMENT
          });
        },
        2000
      )
  }
}

Aby używać tej funkcji - musimy ją zamapować do własności (podobnie jak we wcześniejszych przykładach - funkcją mapDispatchToProps):

const mapDispatchToProps = dispatch =>
 bindActionCreators(
   { increment, },
   dispatch
 )

export default connect(mapStateToProps, mapDispatchToProps)(App);

Nasza reakcja na kliknięcie sprowadza się teraz do wywołania

    this.props.increment();

Dodatkowo obserwując konsolę możemy zobaczyć wynik działania loggera.

Zobacz przykład [029]

Asynchroniczny odczyt danych

Funkcje uruchamiane przez react_thunk doskonale nadają się do odczytywania danych asynchronicznie z plików lub z internetu.

Dodajmy do naszego przykładu funkcję odczytu początkowej wartości licznika (readIniCounter). Wykorzystamy do tego popularną bibliotekę axios:

import axios from 'axios';

export const readIniCounter = () => {
  return async dispatch => {
    let request = axios.get('/stan.json')
     .then(
       res => {
         dispatch( {
           type: START,
           start: res.data.licznik
         });
       })
    }
}

Założyliśmy - jak widać istnienie dodatkowej akcji START:

export const START = 'licznik/START';

....
  switch (action.type) {
    case START:
      return { ...state, licznik: action.start }
....

Stwórzmy plik parametrów w głównym katalogu (/stan.json - zob. parametr axios.get):

{
  "licznik" :10
}

W module App.js importujemy funkcję odczytu ...

import {increment, readIniCounter} from './licznik';

... i dodajemy ją do mapowania:

const mapDispatchToProps = dispatch =>
 bindActionCreators(
   { increment, readIniCounter },
   dispatch
 )

Dobrym miejscem na jej uruchomienie jest componentDidMount (zob. cykl życia komponentu):

  componentDidMount = () => {
    this.props.readIniCounter();
  }

Zobacz przykład [030]