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]