An epic saga about a small custom hook for React (generators, sagas, rxjs) part 3

Part 1. Custom Hook





Part 2. Generators





Redux-saga

This is a middleware for managing side effects when working with redux. It is based on the mechanism of generators. Those. the code is paused until a certain operation with the effect is performed - it is an object with a certain type and data.





One can imagine redux-saga (middleware) as the administrator of the storage chambers. You can put effects in the lockers for an indefinite period and pick them up from there when needed. There is such a messenger put , who comes to the dispatcher and asks to put a message (effect) in the storage chamber. There is such a messenger take , which comes to the dispatcher and asks him to issue a message with a certain type (effect). The dispatcher, at the request of take , looks at all the storage chambers, and if this data is not present, then take remains with the dispatcher and waits until put brings data with the type required for take . There are different types of such messengers (takeEvery, etc.).





The main idea of ​​storage chambers is to separate the sender and the receiver in time (a kind of analogue of asynchronous processing).





Redux-saga is just a tool, but the main thing here is the one who sends all these messengers and processes the data they bring. This "someone" is the generator function (I'll call it the passenger), which is called saga in the help and is passed when the middleware starts . You can run middleware in two ways: using middleware.run (saga, ... args) and runSaga (options, saga, ... args). Saga is a generator function with effects processing logic.





I was interested in the possibility of using redux-saga to handle external events without redux. Let me consider the runSaga (...) method in more detail:





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





  • how to use redux-saga without redux





  • how to use redux-saga to manage hook state









Sandbox link 





Repository link 









To be continued ... RxJS ...












All Articles