Teil 1. Benutzerdefinierter Haken
Redux-Saga
Dies ist eine Middleware zum Verwalten von Nebenwirkungen bei der Arbeit mit Redux. Es basiert auf dem Mechanismus der Generatoren. Jene. Der Code wird angehalten, bis eine bestimmte Operation mit dem Effekt ausgeführt wird - es handelt sich um ein Objekt mit einem bestimmten Typ und bestimmten Daten.
Man kann sich Redux-Saga (Middleware) als Administrator der Lagerkammern vorstellen . Sie können Effekte auf unbestimmte Zeit in die Schließfächer legen und bei Bedarf von dort abholen. Es ist so ein Bote Put , der an den Dispatcher kommt und fragt eine Meldung (Effekt) in der Vorratskammer zu bringen. Es gibt eine solche Messenger- Einstellung , die zum Dispatcher kommt und ihn auffordert, eine Nachricht mit einem bestimmten Typ (Effekt) auszugeben. Der Dispatcher überprüft auf Anforderung der Entnahme alle Lagerkammern. Wenn diese Daten nicht vorhanden sind , verbleibt die Entnahme beim Disponenten und wartet, bis die Übergabe Daten mit dem für die Entnahme erforderlichen Typ liefert . Es gibt verschiedene Arten solcher Boten (takeEvery usw.).
Die Hauptidee von Speicherkammern besteht darin, Sender und Empfänger zeitlich zu "trennen" (eine Art Analogon der asynchronen Verarbeitung).
Die Redux-Saga ist nur ein Werkzeug, aber die Hauptsache hier ist die, die all diese Boten sendet und die Daten verarbeitet, die sie bringen. Dieses "Jemand" ist eine Generatorfunktion (ich nenne es einen Passagier), die in der Hilfe als Saga bezeichnet wird und beim Start der Middleware übergeben wird . Sie können Middleware auf zwei Arten ausführen : mit middleware.run (saga, ... args) und runSaga (options, saga, ... args). Saga ist eine Generatorfunktion mit Effektverarbeitungslogik.
Ich war an der Möglichkeit interessiert, Redux-Saga zu verwenden, um externe Ereignisse ohne Redux zu behandeln. Lassen Sie mich die runSaga (...) -Methode genauer betrachten:
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
wie man Redux-Saga ohne Redux benutzt
Wie verwende ich Redux-Saga, um den Hook-Status zu verwalten?
Sandbox- Link
Repository- Link
Fortsetzung folgt ... RxJS ...