[JavaScript, ReactJS] Эпическая сага про маленький custom hook для React (генераторы, sagas, rxjs) часть 3
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Часть 1. Кастомный хукЧасть 2. ГенераторыRedux-sagaЭто middleware для управления сайд эффектами при работе с redux. В основе лежит механизм генераторов. Т.е. код ставится на паузу пока не будет выполнена определенная операция с эффектом - это объект с определенным типом и данными. Можно представить себе redux-saga (middleware) как администратора камер хранения. В камеры хранения можно класть эффекты на неопределенный срок и забирать их оттуда, когда будет нужно. Есть такой посыльный put, который приходит к диспетчеру и просит положить в камеру хранения сообщение (эффект). Есть такой посыльный take, который приходит к диспетчеру и просит ему выдать сообщение с определенным типом (эффект). Диспетчер, по просьбе take, смотрит все камеры хранения и если этих данных нет, то take остаётся с диспетчером и ждёт, пока put не принесёт данные с нужным для take типом. Существуют разные виды таких посыльных (takeEvery и т.д.). Основная идея камер хранения - это "развести" во времени отправителя и получателя (некий аналог асинхронных обработки).Redux-saga - это просто инструмент, а вот главным тут является тот, кто посылает всех этих посыльных и обрабатывает данные, которые они приносят. Этим "кто-то" является функция-генератор (назову её пассажир), которая в справке называется saga и передаётся при запуске middleware. Запустить middleware можно двумя способами: с помощью middleware.run(saga, ...args) и runSaga(options, saga, ...args). Saga - это функция-генератор с логикой обработки эффектов.Меня заинтересовала возможность использования 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 за следующим сообщением от eventChannelTake забирает следующее сообщение у диспетчера, который взял его из буфера.Круг замкнулся.Исходный код хука с 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В варианте 6 сага использовалась исключительно как менеджер событий. Мне хотелось подключить его к управлению state хука. Погуглив нашёл такой вариант хука 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-saga без redux
- как использовать redux-saga для управления состоянием хука
Ссылка на песочницуСсылка на репозиторийПродолжение следует... RxJS...
===========
Источник:
habr.com
===========
Похожие новости:
- [JavaScript, Программирование, Node.JS] Дино (Deno): Создать API для отдыха с помощью JWT (перевод)
- [JavaScript, ReactJS] Эпическая сага про маленький custom hook для React (генераторы, sagas, rxjs) часть 2
- [JavaScript, Программирование, TypeScript] Кастомизация компонентов Ant Design и оптимизация бандла
- [Python, JavaScript, Браузеры] Brython: заменяем JavaScript на Python на фронтенде (перевод)
- [JavaScript, ReactJS] Эпическая сага про маленький custom hook для React (генераторы, sagas, rxjs)
- [JavaScript, Node.JS, TypeScript] Оптимизация трафика при синхронизация состояний через Jsonpatch
- [Разработка веб-сайтов, JavaScript, Программирование] Сниппет, расширение для VSCode и CLI. Часть 2
- [JavaScript, Интерфейсы, ReactJS, TypeScript] Использование Effector в стеке React + TypeScript
- [JavaScript, Реверс-инжиниринг, Софт] Frida изучаем эксплуатацию алгоритмов Heap
- [Информационная безопасность] Безопасность в диком поле IoT. Первый опыт атаки по побочным каналам
Теги для поиска: #_javascript, #_reactjs, #_reduxsaga, #_put, #_take, #_channel, #_eventchannel, #_hook, #_javascript, #_reactjs
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 02:36
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Часть 1. Кастомный хукЧасть 2. ГенераторыRedux-sagaЭто middleware для управления сайд эффектами при работе с redux. В основе лежит механизм генераторов. Т.е. код ставится на паузу пока не будет выполнена определенная операция с эффектом - это объект с определенным типом и данными. Можно представить себе redux-saga (middleware) как администратора камер хранения. В камеры хранения можно класть эффекты на неопределенный срок и забирать их оттуда, когда будет нужно. Есть такой посыльный put, который приходит к диспетчеру и просит положить в камеру хранения сообщение (эффект). Есть такой посыльный take, который приходит к диспетчеру и просит ему выдать сообщение с определенным типом (эффект). Диспетчер, по просьбе take, смотрит все камеры хранения и если этих данных нет, то take остаётся с диспетчером и ждёт, пока put не принесёт данные с нужным для take типом. Существуют разные виды таких посыльных (takeEvery и т.д.). Основная идея камер хранения - это "развести" во времени отправителя и получателя (некий аналог асинхронных обработки).Redux-saga - это просто инструмент, а вот главным тут является тот, кто посылает всех этих посыльных и обрабатывает данные, которые они приносят. Этим "кто-то" является функция-генератор (назову её пассажир), которая в справке называется saga и передаётся при запуске middleware. Запустить middleware можно двумя способами: с помощью middleware.run(saga, ...args) и runSaga(options, saga, ...args). Saga - это функция-генератор с логикой обработки эффектов.Меня заинтересовала возможность использования 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(
{ channel: sagaChannelRef.current, dispatch: () => {}, getState: () => {}, }, 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)); } yield take(eventsChannel);
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"; }); } useEffect(() => {
... sagaChannelRef.current.put({ type: "STATE_UPDATED" }); ... }, [state]); 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; 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]; } 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); } } 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; 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;
=========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 02:36
Часовой пояс: UTC + 5