Redux-saga
これは、reduxを使用する際の副作用を管理するためのミドルウェアです。これは、ジェネレーターのメカニズムに基づいています。それら。コードは、エフェクトを使用した特定の操作が実行されるまで一時停止されます。これは、特定のタイプとデータを持つオブジェクトです。
貯蔵室の管理者としてredux-saga(ミドルウェア)を想像することができます。ロッカーにエフェクトを無期限に入れて、必要に応じてそこから拾うことができます。そのようなメッセンジャープットがあり、ディスパッチャーに来て、貯蔵室にメッセージ(効果)を入れるように頼みます。そのようなメッセンジャーテイクがあり、ディスパッチャーに来て、特定のタイプ(効果)のメッセージを発行するように依頼します。ディスパッチャは、テイクの要求に応じて、すべての保管室を調べ、このデータが存在しない場合、テイクはディスパッチャに残り、プットがテイクに必要なタイプのデータをもたらすまで待機します。このようなメッセンジャーにはさまざまな種類があります(takeEveryなど)。
貯蔵室の主なアイデアは、送信者と受信者を時間内に「分離」することです(非同期処理の一種のアナログ)。
Redux-sagaは単なるツールですが、ここで重要なのは、これらすべてのメッセンジャーを送信し、それらがもたらすデータを処理するものです。この「誰か」はジェネレーター関数(私はそれを乗客と呼びます)であり、ヘルプでは佐賀と呼ばれ、ミドルウェアの起動時に渡されます。ミドルウェアは、middleware.run(saga、... args)とrunSaga(options、saga、... args)の2つの方法で実行できます。佐賀はエフェクト処理ロジックを備えたジェネレーター関数です。
redux-sagaを使用してreduxなしで外部イベントを処理する可能性に興味がありました。runSaga(...)メソッドについて詳しく考えてみましょう。
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
reduxなしでredux-sagaを使用する方法
redux-sagaを使用してフック状態を管理する方法
サンドボックスリンク
リポジトリリンク
続く... RxJS..。