Redux Saga to popularny middleware dla Reduxa, oparty na generatorach. Umożliwia obsługę asynchroniczności w sposób przypominający kod synchroniczny.
Warto poznać tą bibliotekę ze względu na prostotę tworzonego przy jej użyciu kodu – Sagi są dobrym miejscem na izolację logiki naszej aplikacji, oddzielenie jej od UI i od samego Reduxa.
Można pokusić się o pytanie, czemu zamiast Redux Saga nie użyć Redux Thunk wraz ze składnią async / await? Przewagą Sag jest możliwość tworzenia bardziej skomplikowanego flow obejmujące chociażby oczekiwanie na pojawienie się konkretnej akcji.
Generatory
Generatory, które są używane do tworzenia Sag, są funkcjami które uruchamiają się do pewnego momentu po czym „zamarzają”, a od momentu zatrzymania można je uruchomić dalej.
Przykład generatora
Spójrzmy na przykład generatora:
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function* createGenerator(){
let variable = 1;
yield variable;
variable+=1;
yield variable;
variable = `${variable} as string`;
yield variable;
variable = `${variable} but with done: true`;
return variable; //return ends generator
yield'something'
}
function* createGenerator() {
let variable = 1;
yield variable;
variable+=1;
yield variable;
variable = `${variable} as string`;
yield variable;
variable = `${variable} but with done: true`;
return variable; //return ends generator
yield 'something'
}
function* createGenerator() {
let variable = 1;
yield variable;
variable+=1;
yield variable;
variable = `${variable} as string`;
yield variable;
variable = `${variable} but with done: true`;
return variable; //return ends generator
yield 'something'
}
Wywołanie takiej funkcji spowoduje utworzenie obiektu typu Generator:
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const generator = createGenerator();
const generator = createGenerator();
const generator = createGenerator();
Generator taki posiada metodę next() która zwróci obiekt z 2 polami – polem value zawierającą wartość wywołaną przez yield oraz polem done, oznaczającą czy generator się zakończył. Kolejne wywołania metody next na zakończonym generatorze będą skutkować zwróceniem obiektu z value ustawionym na undefined i done ustawionym na true. Nie dojdziemy więc do ostatniej linijki z yield something.
Sprawdźmy działanie takiego generatora poprzez wypisywanie rezultatów tego wywołań, mając taki kod HTML:
Utwórzmy aplikację CRA, która zawierać będzie Reacta, Reduxa i Redux Sagę. Najszybciej będzie skorzystać z gotowego szablonu do create-react-app:
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
npx create-react-app my-app --template redux
npx create-react-app my-app --template redux
npx create-react-app my-app --template redux
Jak możemy zauważyć w kodzie wygenerowanej aplikacji, używa ona biblioteki Redux Toolkit, co widać m.in. w kodzie pliku counterSlice:
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import{ createSlice } from '@reduxjs/toolkit';
exportconst counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0,
},
reducers: {
increment: state =>{
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn't actually mutate the state because it uses the Immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
state.value += 1;
},
// inne reducery
},
});
// reszta kodu
import { createSlice } from '@reduxjs/toolkit';
export const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0,
},
reducers: {
increment: state => {
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn't actually mutate the state because it uses the Immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
state.value += 1;
},
// inne reducery
},
});
// reszta kodu
import { createSlice } from '@reduxjs/toolkit';
export const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0,
},
reducers: {
increment: state => {
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn't actually mutate the state because it uses the Immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
state.value += 1;
},
// inne reducery
},
});
// reszta kodu
Użycie tej biblioteki uprości nam znacznie kod i zredukuje charakterystyczny dla Reduxa boilerplate. Nie będę w poście omawiał tej biblioteki szczegółowo, jeżeli chcesz dowiedzieć się o niej więcej możesz zapisać się na moją listę mailową i otrzymać dokument opisujący bibliotekę:
Zainstalujmy również bibliotekę Redux Saga:
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
npm install --save redux-saga
npm install --save redux-saga
npm install --save redux-saga
Przykładowy kod zawiera aplikację Countera, dodajmy do niej asynchroniczność pod postacią zapisywania historii, symulując wysyłanie zapytań na serwer. Utwórzmy plik onCounterChangedSaga który zawierać będzie kod naszej Sagi.
Ponieważ będziemy chcieli reagować na akcje zmieniające wartość countera, musimy zaimportować metodę takeEvery, która jako pierwszy parametr przyjmuje akcje na jakie ma reagować a drugim parametrem jest funkcja, jaka ma być wywoływana:
Funkcję onCounterChanged napiszemy za chwilę, skupmy się w tym momencie na pierwszym parametrze, liście akcji na jakie chcemy się zapiąć. Klasyczny kod Reduxa wyglądałby tak:
Gdzie typy akcji zadeklarowane są explicit, np jako stałe. Natomiast jeżeli używamy Redux Toolkit, akcje (jak już wiesz z dokumentu ) typ akcji jest tworzony przez bibliotekę, dostęp do niego mamy poprzez action.toString() i nie musimy sami tego deklarować – to kolejna korzyść ze stosowania Redux Toolkit.
Gdy mamy już przechwyconą akcję, chcemy „zapisać” ją na serwerze. Napiszmy więc kod symulujący żądanie na serwer, odpowiadający dotychczas zapisanymi akcjami:
// Need to copy array because usage as resolve parameter makes it not extensible
resolve([...actions])
})
const actions = [];
const addActionRequest = (action) => new Promise((resolve, reject) => {
actions.push({
...action,
id: Date.now(),
});
// Need to copy array because usage as resolve parameter makes it not extensible
resolve([...actions])
})
const actions = [];
const addActionRequest = (action) => new Promise((resolve, reject) => {
actions.push({
...action,
id: Date.now(),
});
// Need to copy array because usage as resolve parameter makes it not extensible
resolve([...actions])
})
Zapytanie to wywołamy za pomocą funkcji call, która jako pierwszy parametr przyjmuje asynchroniczną funkcję do wywołania a jako kolejne parametry to co ma być do funkcji przekazane:
Ponieważ flow jaki chcemy uzyskać, na potrzeby tego przykładu, jest następujący:
Użytkownik klika przycisk countera
Wykonywane jest zapytanie na serwer
Aplikacja pyta użytkownika czy chce załadować nowe dane
Dane są ładowane jeśli ten się zgodzi
Musimy dodać nowe akcje w pliku counterSlice i dodać je do reducerów:
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
setOperations: (state, action)=>{
// save and show history
state.operations = action.payload;
},
showAgreement: (state, action)=>{
// show agreement modal
state.agreementVisible = true;
},
applyHistoryDecision: (state, action)=>{
// hide agreement modal after decision
state.agreementVisible = false;
},
setOperations: (state, action) => {
// save and show history
state.operations = action.payload;
},
showAgreement: (state, action) => {
// show agreement modal
state.agreementVisible = true;
},
applyHistoryDecision: (state, action) => {
// hide agreement modal after decision
state.agreementVisible = false;
},
setOperations: (state, action) => {
// save and show history
state.operations = action.payload;
},
showAgreement: (state, action) => {
// show agreement modal
state.agreementVisible = true;
},
applyHistoryDecision: (state, action) => {
// hide agreement modal after decision
state.agreementVisible = false;
},
Oraz dodać do eksportu. W pliku Counter.js pobieramy interesujące nas dane, czyli historię operacji oraz to, czy ma wyświetlać się zapytanie o wyświetlenie świeżo pobranej historii:
Następnie, wracając do sagi, dodajemy kod odpowiedzialny za zdispatchowanie akcji pokazującej zapytanie za pomocą metody put, która wrzuca akcję do Reduxa:
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
yieldput(showAgreement());
yield put(showAgreement());
yield put(showAgreement());
Jeśli chcemy poczekać na pojedynczą akcję, możemy użyć metody take. Metoda ta jako parametr przyjmuje typ akcji a zwraca tą akcję – wraz z payload, które będzie nam potrzebne:
Lektura tego postu da Ci podstawy do dalszego zagłębiania się w bibliotekę Redux Saga. Jej sposób obsługi kodu asynchronicznego oraz możliwość zamknięcia logiki biznesowej i oddzielenie jej od kodu komponentów oraz kodu Reduxa powodują, że dobrze ją znać.