ملحمة ملحمية حول خطاف مخصص صغير لـ React (مولدات ، Sagas ، rxjs) الجزء 3

الجزء 1. هوك مخصص





الجزء 2. المولدات





إعادة الملحمة

هذه برمجية وسيطة لإدارة الآثار الجانبية عند العمل مع redux. يعتمد على آلية المولدات. أولئك. يتم إيقاف الكود مؤقتًا حتى يتم تنفيذ عملية معينة مع التأثير - إنه كائن بنوع وبيانات معينة.





يمكن للمرء أن يتخيل redux-saga (البرامج الوسيطة) كمسؤول عن غرف التخزين. يمكنك وضع التأثيرات في الخزائن لفترة غير محددة واستلامها من هناك عند الحاجة. هناك مثل هذا رسول وضع ، الذي يأتي إلى المرسل ويسأل لوضع رسالة (تأثير) في غرفة التخزين. هناك مثل هذا رسول تأخذ ، والتي تأتي إلى المرسل ويطلب منه أن يصدر رسالة مع نوع معين (المفعول). يقوم المرسل ، بناءً على طلب الاستلام ، بالنظر في جميع غرف التخزين ، وإذا لم تكن هذه البيانات موجودة ، فقم بأخذ يبقى مع المرسل وينتظر حتى يجلب وضع البيانات بالنوع المطلوب لأخذها . هناك أنواع مختلفة من هؤلاء الرسل (takeEvery ، إلخ).





الفكرة الرئيسية لغرف التخزين هي "فصل" المرسل والمستقبل في الوقت المناسب (نوع من المعالجة التناظرية غير المتزامنة).





تعد Redux-saga مجرد أداة ، ولكن الشيء الرئيسي هنا هو الشخص الذي يرسل كل هؤلاء الرسل ويعالج البيانات التي يجلبونها. هذا "الشخص" هو وظيفة المولد (سأطلق عليه اسم الراكب) ، والتي تسمى الملحمة في المساعدة ويتم تمريرها عند بدء البرنامج الوسيط . يمكنك تشغيل البرامج الوسيطة بطريقتين: استخدام middleware.run (saga ، ... args) و runSaga (options ، saga ، ... args). Saga هي وظيفة مولد مع منطق معالجة التأثيرات.





كنت مهتمًا بإمكانية استخدام redux-saga للتعامل مع الأحداث الخارجية دون إعادة. دعني أفكر في طريقة 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

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-saga لإدارة حالة الخطاف









رابط  Sandbox





رابط  المستودع









يتبع ... RxJS ...












All Articles