Redux-saga
Este é um middleware para gerenciar efeitos colaterais ao trabalhar com redux. É baseado no mecanismo de geradores. Essa. o código é pausado até que uma determinada operação com o efeito seja realizada - é um objeto com um certo tipo e dados.
Pode-se imaginar o redux-saga (middleware) como o administrador das câmaras de armazenamento. Você pode colocar os efeitos nos armários por um período indeterminado e retirá-los quando necessário. Existe um tal mensageiro posto , que chega ao despachante e pede para colocar uma mensagem (efeito) na câmara de armazenamento. Existe uma tal tomada de mensageiro , que chega ao despachante e pede-lhe para emitir uma mensagem com um determinado tipo (efeito). O despachante, a pedido de take , examina todas as câmaras de armazenamento, e se estes dados não estiverem presentes, então take fica com o despachante e espera até que put traga dados com o tipo requerido para take . Existem diferentes tipos de mensageiros (takeEvery, etc.).
A ideia principal das câmaras de armazenamento é "separar" o remetente e o receptor no tempo (uma espécie de análogo do processamento assíncrono).
Redux-saga é apenas uma ferramenta, mas o principal aqui é aquele que envia todos esses mensageiros e processa os dados que eles trazem. Esse "alguém" é uma função geradora (vou chamá-la de passageiro), que é chamada de saga na ajuda e é passada quando o middleware é iniciado . Você pode executar o middleware de duas maneiras: usando middleware.run (saga, ... args) e runSaga (opções, saga, ... args). Saga é uma função geradora com lógica de processamento de efeitos.
Eu estava interessado na possibilidade de usar redux-saga para lidar com eventos externos sem redux. Deixe-me considerar o método runSaga (...) em mais detalhes:
runSaga(options, saga, ...args)
saga - , ;
args - , saga;
options - , "" redux-saga. :
channel - , ;
dispatch - , , redux-saga put.
getState - , state, redux-saga. state.
6. Redux-saga
saga . channel ( ) redux-saga. , - eventsChannel. ! .
(channel), (redux-saga)
const sagaChannelRef = useRef(stdChannel());
runSaga() redux-saga .
runSaga(
{
channel: sagaChannelRef.current,
dispatch: () => {},
getState: () => {},
},
saga
);
(channel), (redux-saga) ( - saga)
(- saga) ( ).
const eventsChannel = yield call(getImageLoadingSagas, imgArray);
function getImageLoadingSagas(imagesArray) {
return eventChannel((emit) => {
for (const img of imagesArray) {
const imageChecker = new Image();
imageChecker.addEventListener("load", () => {
emit(true);
});
imageChecker.addEventListener("error", () => {
emit(true);
});
imageChecker.src = img.url;
}
setTimeout(() => {
//
emit(END);
}, 100000);
return () => {
};
}, buffers.expanding(10));
}
.. (- saga) (redux-saga) put, (eventsChannel). (eventChannel) (redux-saga) , , take, .
yield take(eventsChannel);
(redux-saga) eventChannel, take, (- saga). take .
(- saga) (- putCounter) call(). , saga (- saga) , putCounter (- putCounter) (.. saga , putCounter).
yield call(putCounter);
function* putCounter() {
dispatch({
type: ACTIONS.SET_COUNTER,
data: stateRef.current.counter + stateRef.current.counterStep,
});
yield take((action) => {
return action.type === "STATE_UPDATED";
});
}
putCounter (- putCounter). take (redux-saga) STATE_UPDATED .
( ).
take(eventChannel) ( - saga) saga (- saga). saga (- saga) putCounter (- putCounter) . putCounter (- putCounter), , take, (redux-saga) put, STATE_UPDATED. ", ".
"" - STATE_UPDATED. , eventChannel . eventChannel, (redux-saga). , () eventChannel.
put useEffect
useEffect(() => {
...
sagaChannelRef.current.put({ type: "STATE_UPDATED" });
...
}, [state]);
put STATE_UPDATED (redux-saga).
(redux-saga) take, putCounter.
putCounter saga, .
saga, take eventChannel
Take , .
.
redux-saga
import { useReducer, useEffect, useRef } from "react";
import { reducer, initialState, ACTIONS } from "./state";
import { runSaga, eventChannel, stdChannel, buffers, END } from "redux-saga";
import { call, take } from "redux-saga/effects";
const PRELOADER_SELECTOR = ".preloader__wrapper";
const PRELOADER_COUNTER_SELECTOR = ".preloader__counter";
const usePreloader = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const stateRef = useRef(state);
const sagaChannelRef = useRef(stdChannel());
const preloaderEl = document.querySelector(PRELOADER_SELECTOR);
const counterEl = document.querySelector(PRELOADER_COUNTER_SELECTOR);
useEffect(() => {
const imgArray = document.querySelectorAll("img");
if (imgArray.length > 0) {
dispatch({
type: ACTIONS.SET_COUNTER_STEP,
data: Math.floor(100 / imgArray.length) + 1,
});
function* putCounter() {
dispatch({
type: ACTIONS.SET_COUNTER,
data: stateRef.current.counter + stateRef.current.counterStep,
});
yield take((action) => {
return action.type === "STATE_UPDATED";
});
}
function* saga() {
const eventsChannel = yield call(getImageLoadingSagas, imgArray);
try {
while (true) {
yield take(eventsChannel);
yield call(putCounter);
}
} finally {
//channel closed
}
}
runSaga(
{
channel: sagaChannelRef.current,
dispatch: () => {},
getState: () => {},
},
saga
);
}
}, []);
useEffect(() => {
stateRef.current = state;
if (stateRef.current.counterStep != 0 && stateRef.current.counter != 0) {
sagaChannelRef.current.put({ type: "STATE_UPDATED" });
}
if (counterEl) {
stateRef.current.counter < 100
? (counterEl.innerHTML = `${stateRef.current.counter}%`)
: hidePreloader(preloaderEl);
}
}, [state]);
return;
};
function getImageLoadingSagas(imagesArray) {
return eventChannel((emit) => {
for (const img of imagesArray) {
const imageChecker = new Image();
imageChecker.addEventListener("load", () => {
emit(true);
});
imageChecker.addEventListener("error", () => {
emit(true);
});
imageChecker.src = img.url;
}
setTimeout(() => {
//
emit(END);
}, 100000);
return () => {
};
}, buffers.expanding(10));
}
const hidePreloader = (preloaderEl) => {
preloaderEl.remove();
};
export default usePreloader;
, . , .
7. Redux-saga + useReducer = useReducerAndSaga
,
useReducerAndSaga.js
import { useReducer, useEffect, useRef } from "react";
import { runSaga, stdChannel, buffers } from "redux-saga";
export function useReducerAndSaga(reducer, state0, saga, sagaOptions) {
const [state, reactDispatch] = useReducer(reducer, state0);
const sagaEnv = useRef({ state: state0, pendingActions: [] });
function dispatch(action) {
console.log("useReducerAndSaga: react dispatch", action);
reactDispatch(action);
console.log("useReducerAndSaga: post react dispatch", action);
// dispatch to sagas is done in the commit phase
sagaEnv.current.pendingActions.push(action);
}
useEffect(() => {
console.log("useReducerAndSaga: update saga state");
// sync with react state, *should* be safe since we're in commit phase
sagaEnv.current.state = state;
const pendingActions = sagaEnv.current.pendingActions;
// flush any pending actions, since we're in commit phase, reducer
// should've handled all those actions
if (pendingActions.length > 0) {
sagaEnv.current.pendingActions = [];
console.log("useReducerAndSaga: flush saga actions");
pendingActions.forEach((action) => sagaEnv.current.channel.put(action));
sagaEnv.current.channel.put({ type: "REACT_STATE_READY", state });
}
});
// This is a one-time effect that starts the root saga
useEffect(() => {
sagaEnv.current.channel = stdChannel();
const task = runSaga(
{
...sagaOptions,
channel: sagaEnv.current.channel,
dispatch,
getState: () => {
return sagaEnv.current.state;
}
},
saga
);
return () => task.cancel();
}, []);
return [state, dispatch];
}
sagas.js
sagas.js
import { eventChannel, buffers } from "redux-saga";
import { call, select, take, put } from "redux-saga/effects";
import { ACTIONS, getCounterStep, getCounter, END } from "./state";
export const getImageLoadingSagas = (imagesArray) => {
return eventChannel((emit) => {
for (const img of imagesArray) {
const imageChecker = new Image();
imageChecker.addEventListener("load", () => {
emit(true);
});
imageChecker.addEventListener("error", () => {
emit(true);
});
imageChecker.src = img.src;
}
setTimeout(() => {
//
emit(END);
}, 100000);
return () => {};
}, buffers.fixed(20));
};
function* putCounter() {
const currentCounter = yield select(getCounter);
const counterStep = yield select(getCounterStep);
yield put({ type: ACTIONS.SET_COUNTER, data: currentCounter + counterStep });
yield take((action) => {
return action.type === "REACT_STATE_READY";
});
}
function* launchLoadingEvents(imgArray) {
const eventsChannel = yield call(getImageLoadingSagas, imgArray);
while (true) {
yield take(eventsChannel);
yield call(putCounter);
}
}
export function* saga() {
while (true) {
const { data } = yield take(ACTIONS.SET_IMAGES);
yield call(launchLoadingEvents, data);
}
}
state. action SET_IMAGES counter counterStep
state.js
const SET_COUNTER = "SET_COUNTER";
const SET_COUNTER_STEP = "SET_COUNTER_STEP";
const SET_IMAGES = "SET_IMAGES";
export const initialState = {
counter: 0,
counterStep: 0,
images: [],
};
export const reducer = (state, action) => {
switch (action.type) {
case SET_IMAGES:
return { ...state, images: action.data };
case SET_COUNTER:
return { ...state, counter: action.data };
case SET_COUNTER_STEP:
return { ...state, counterStep: action.data };
default:
throw new Error("This action is not applicable to this component.");
}
};
export const ACTIONS = {
SET_COUNTER,
SET_COUNTER_STEP,
SET_IMAGES,
};
export const getCounterStep = (state) => state.counterStep;
export const getCounter = (state) => state.counter;
, usePreloader .
usePreloader.js
import { useEffect } from "react";
import { reducer, initialState, ACTIONS } from "./state";
import { useReducerAndSaga } from "./useReducerAndSaga";
import { saga } from "./sagas";
const PRELOADER_SELECTOR = ".preloader__wrapper";
const PRELOADER_COUNTER_SELECTOR = ".preloader__counter";
const usePreloader = () => {
const [state, dispatch] = useReducerAndSaga(reducer, initialState, saga);
const preloaderEl = document.querySelector(PRELOADER_SELECTOR);
const counterEl = document.querySelector(PRELOADER_COUNTER_SELECTOR);
useEffect(() => {
const imgArray = document.querySelectorAll("img");
if (imgArray.length > 0) {
dispatch({
type: ACTIONS.SET_COUNTER_STEP,
data: Math.floor(100 / imgArray.length) + 1,
});
dispatch({
type: ACTIONS.SET_IMAGES,
data: imgArray,
});
}
}, []);
useEffect(() => {
if (counterEl) {
state.counter < 100
? (counterEl.innerHTML = `${state.counter}%`)
: hidePreloader(preloaderEl);
}
}, [state.counter]);
return;
};
const hidePreloader = (preloaderEl) => {
preloaderEl.remove();
};
export default usePreloader;
:
redux-saga
como usar redux-saga sem redux
como usar o redux-saga para gerenciar o estado do gancho
Link da sandbox
Link do repositório
Continua ... RxJS ...