Redux-saga
Este es un software intermedio para administrar los efectos secundarios cuando se trabaja con redux. Se basa en el mecanismo de los generadores. Aquellos. el código se pone en pausa hasta que se realiza una determinada operación con el efecto: es un objeto con un cierto tipo y datos.
Uno puede imaginar a redux-saga (middleware) como el administrador de las cámaras de almacenamiento. Puede poner efectos en los casilleros por un período indefinido y recogerlos de allí cuando sea necesario. Hay tal mensajero puesto , que viene al despachador y le pide poner un mensaje (efecto) en la cámara de almacenamiento. Existe una toma de mensajería de este tipo , que llega al despachador y le pide que emita un mensaje con un cierto tipo (efecto). El despachador, a petición de take , mira todas las cámaras de almacenamiento, y si estos datos no están presentes, take se queda con el despachador y espera hasta que put trae datos con el tipo requerido para take . Hay diferentes tipos de tales mensajeros (takeEvery, etc.).
La idea principal de las cámaras de almacenamiento es "separar" el emisor y el receptor en el tiempo (una especie de análogo del procesamiento asíncrono).
Redux-saga es solo una herramienta, pero lo principal aquí es quien envía todos estos mensajeros y procesa los datos que traen. Este "alguien" es la función del generador (lo llamaré el pasajero), que se llama saga en la ayuda y se pasa cuando se inicia el middleware . Puede ejecutar middleware de dos formas: usando middleware.run (saga, ... args) y runSaga (options, saga, ... args). Saga es una función generadora con lógica de procesamiento de efectos.
Estaba interesado en la posibilidad de usar redux-saga para manejar eventos externos sin redux. Permítanme considerar el método runSaga (...) con más detalle:
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
cómo usar redux-saga sin redux
cómo usar redux-saga para administrar el estado del gancho
Enlace de zona de pruebas
Enlace de repositorio
Continuará ... RxJS ...