Uma saga épica sobre um pequeno gancho personalizado para React (geradores, sagas, rxjs) parte 3

Parte 1. Gancho personalizado





Parte 2. Geradores





Redux-saga

Este é um middleware para gerenciar efeitos colaterais ao trabalhar com redux. É baseado no mecanismo de geradores. Essa. o código é pausado até que uma determinada operação com o efeito seja realizada - é um objeto com um certo tipo e dados.





Pode-se imaginar o redux-saga (middleware) como o administrador das câmaras de armazenamento. Você pode colocar os efeitos nos armários por um período indeterminado e retirá-los quando necessário. Existe um tal mensageiro posto , que chega ao despachante e pede para colocar uma mensagem (efeito) na câmara de armazenamento. Existe uma tal tomada de mensageiro , que chega ao despachante e pede-lhe para emitir uma mensagem com um determinado tipo (efeito). O despachante, a pedido de take , examina todas as câmaras de armazenamento, e se estes dados não estiverem presentes, então take fica com o despachante e espera até que put traga dados com o tipo requerido para take . Existem diferentes tipos de mensageiros (takeEvery, etc.).





A ideia principal das câmaras de armazenamento é "separar" o remetente e o receptor no tempo (uma espécie de análogo do processamento assíncrono).





Redux-saga é apenas uma ferramenta, mas o principal aqui é aquele que envia todos esses mensageiros e processa os dados que eles trazem. Esse "alguém" é uma função geradora (vou chamá-la de passageiro), que é chamada de saga na ajuda e é passada quando o middleware é iniciado . Você pode executar o middleware de duas maneiras: usando middleware.run (saga, ... args) e runSaga (opções, saga, ... args). Saga é uma função geradora com lógica de processamento de efeitos.





Eu estava interessado na possibilidade de usar redux-saga para lidar com eventos externos sem redux. Deixe-me considerar o método runSaga (...) em mais detalhes:





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





  • como usar redux-saga sem redux





  • como usar o redux-saga para gerenciar o estado do gancho









Link da  sandbox





Link do  repositório









Continua ... RxJS ...












All Articles