Una saga épica sobre un pequeño gancho personalizado para React (generators, sagas, rxjs) parte 2

Parte 1. Gancho personalizado





Parte 3. Redux-saga





Sobre generadores

Los generadores son un nuevo tipo de función que se introdujo en ES6. Se han escrito muchos artículos sobre ellos y se dan muchos ejemplos teóricos. En cuanto a mí, el libro No conoces JS , parte de async & performance ayudó a aclarar la esencia de los generadores y cómo usarlos . De todos los libros de JS que he estudiado, este es el más lleno de información útil sin agua.





Imagine que el generador (la función en la declaración, que es *) es una especie de dispositivo eléctrico con un panel de control remoto. Después de crear y montar el generador (declaración de función), debe "girarlo" (ejecutar esta función) para que gire a velocidad de ralentí y "alimente" el panel de control por sí mismo (cuando se ejecuta la función del generador, devuelve un iterador). Este control remoto tiene dos botones: Inicio (llamar al siguiente método del iterador por primera vez) y Siguiente (llamadas posteriores al siguiente método del iterador). Luego, con este panel de control, puede correr por toda la planta de energía (según nuestra aplicación) y cuando necesite energía eléctrica (algunos valores de la función del generador), presione el botón siguiente en el control remoto (ejecute el método next () del generador).El generador produce la cantidad requerida de electricidad (devuelve un cierto valor a través del rendimiento) y vuelve a entrar en modo inactivo (la función del generador espera la próxima llamada del iterador). El bucle continúa mientras el generador pueda producir electricidad (hay declaraciones de rendimiento) o no se detendrá (el retorno se encuentra en la función del generador).





Y en toda esta analogía, el punto clave es el panel de control (iterador). Se puede pasar a diferentes partes de la aplicación y en el momento adecuado "tomar" valores del generador. Para completar la imagen, puede agregar un número ilimitado de botones en el panel de control para iniciar el generador en ciertos modos (pasando parámetros al siguiente método (cualquier parámetro) del iterador), pero dos botones son suficientes para implementar el gancho.





Opción 4. Generador sin promesas

Esta opción se proporciona para mayor claridad, porque Los generadores funcionan con toda su fuerza con promesas (mecanismo asíncrono / espera). Pero esta opción está funcionando y tiene derecho a existir en ciertas situaciones simples.





Creo una variable en el gancho para almacenar la referencia al iterador (celda para el panel de control del generador)





const iteratorRef = useRef(null);
      
      



. . , next() ( next). :





const updateCounter = () => {
  iteratorRef.current.next();
};

const checkImageLoading = (url) => {
  const imageChecker = new Image();
  imageChecker.addEventListener("load", updateCounter);
  imageChecker.addEventListener("error", updateCounter);
  imageChecker.src = url;
};
      
      



. , , , next . , " ". dispatch , . :





function* main() {
  for (let i = 0; i < imgArray.length; i++) {
    checkImageLoading(imgArray[i].src);
  }
  for (let i = 0; i < imgArray.length; i++) {
    yield true;
    dispatch({
      type: ACTIONS.SET_COUNTER,
      data: stateRef.current.counter + stateRef.current.counterStep
    });
  }
}
      
      



"" , ( iteratorRef. ( next ).





.





import { useReducer, useEffect, useLayoutEffect, useRef } from "react";
import { reducer, initialState, ACTIONS } from "./state";

const PRELOADER_SELECTOR = ".preloader__wrapper";
const PRELOADER_COUNTER_SELECTOR = ".preloader__counter";

const usePreloader = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  const stateRef = useRef(state);
  const iteratorRef = useRef(null);

  const preloaderEl = document.querySelector(PRELOADER_SELECTOR);
  const counterEl = document.querySelector(PRELOADER_COUNTER_SELECTOR);

  const updateCounter = () => {
    iteratorRef.current.next();
  };

  const checkImageLoading = (url) => {
    const imageChecker = new Image();
    imageChecker.addEventListener("load", updateCounter);
    imageChecker.addEventListener("error", updateCounter);
    imageChecker.src = url;
  };

  useEffect(() => {
    const imgArray = document.querySelectorAll("img");
    if (imgArray.length > 0) {
      dispatch({
        type: ACTIONS.SET_COUNTER_STEP,
        data: Math.floor(100 / imgArray.length) + 1
      });
    }

    function* main() {
      for (let i = 0; i < imgArray.length; i++) {
        checkImageLoading(imgArray[i].src);
      }
      for (let i = 0; i < imgArray.length; i++) {
        yield true;
        dispatch({
          type: ACTIONS.SET_COUNTER,
          data: stateRef.current.counter + stateRef.current.counterStep
        });
      }
    }

    iteratorRef.current = main();
    iteratorRef.current.next();
  }, []);

  useLayoutEffect(() => {
    stateRef.current = state;

    if (counterEl) {
      stateRef.current.counter < 100
        ? (counterEl.innerHTML = `${stateRef.current.counter}%`)
        : hidePreloader(preloaderEl);
    }
  }, [state]);

  return;
};

const hidePreloader = (preloaderEl) => {
  preloaderEl.remove();
};

export default usePreloader;

      
      







.





5.

. next ( ). ( ).





:





const getImageLoading = async function* (imagesArray) {
  for (const img of imagesArray) {
    yield new Promise((resolve, reject) => {
      const imageChecker = new Image();
      imageChecker.addEventListener("load", () => resolve(true));
      imageChecker.addEventListener("error", () => resolve(true));
      imageChecker.src = img.url;
    });
  }
};

      
      



:





for await (const response of getImageLoading(imgArray)) {
  dispatch({
    type: ACTIONS.SET_COUNTER,
    data: stateRef.current.counter + stateRef.current.counterStep
  });
}
      
      



for await ... of. Next.





- . , , .





import { useReducer, useEffect, useRef } from "react";
import { reducer, initialState, ACTIONS } from "./state";

const PRELOADER_SELECTOR = ".preloader__wrapper";
const PRELOADER_COUNTER_SELECTOR = ".preloader__counter";

const usePreloader = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  const stateRef = useRef(state);

  const preloaderEl = document.querySelector(PRELOADER_SELECTOR);
  const counterEl = document.querySelector(PRELOADER_COUNTER_SELECTOR);

  useEffect(() => {
    async function imageLoading() {
      const imgArray = document.querySelectorAll("img");
      if (imgArray.length > 0) {
        dispatch({
          type: ACTIONS.SET_COUNTER_STEP,
          data: Math.floor(100 / imgArray.length) + 1
        });
  
        for await (const response of getImageLoading(imgArray)) {
          dispatch({
            type: ACTIONS.SET_COUNTER,
            data: stateRef.current.counter + stateRef.current.counterStep
          });
        }
      }
    }
    imageLoading();
  }, []);

  useEffect(() => {
    stateRef.current = state;

    if (counterEl) {
      stateRef.current.counter < 100
        ? (counterEl.innerHTML = `${stateRef.current.counter}%`)
        : hidePreloader(preloaderEl);
    }
  }, [state]);

  return;
};

const getImageLoading = async function* (imagesArray) {
  for (const img of imagesArray) {
    yield new Promise((resolve, reject) => {
      const imageChecker = new Image();
      imageChecker.addEventListener("load", () => resolve(true));
      imageChecker.addEventListener("error", () => resolve(true));
      imageChecker.src = img.url;
    });
  }
};

const hidePreloader = (preloaderEl) => {
  preloaderEl.remove();
};

export default usePreloader;

      
      







:

:





  • useRef ( )





  • cómo controlar el flujo de eventos usando generadores, pero sin usar promesas (usando devoluciones de llamada)





  • cómo gestionar el flujo de eventos con los manejadores prometidos usando generadores y un bucle de espera ...





Enlace de zona de  pruebas





Enlace de  repositorio









Continuará ... redux-saga ...












All Articles