Eine epische Saga über einen kleinen benutzerdefinierten Hook für React (Generatoren, Sagen, RXJs) Teil 3

Teil 1. Benutzerdefinierter Haken





Teil 2. Generatoren





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

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





  • 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 ...












All Articles