Kisah epik tentang hook kustom kecil untuk React (generator, sagas, rxjs) bagian 3

Bagian 1. Hook Kustom





Bagian 2. Generator





Redux-saga

Ini adalah middleware untuk mengelola efek samping saat bekerja dengan redux. Ini didasarkan pada mekanisme generator. Itu. kode dihentikan sementara sampai operasi tertentu dengan efek dilakukan - itu adalah objek dengan tipe dan data tertentu.





Dapat dibayangkan redux-saga (middleware) sebagai administrator ruang penyimpanan. Anda dapat menempatkan efek di loker untuk waktu yang tidak terbatas dan mengambilnya dari sana saat diperlukan. Ada seperti utusan put , yang datang ke operator dan meminta untuk menempatkan pesan (efek) di ruang penyimpanan. Ada semacam messenger take , yang datang ke dispatcher dan memintanya untuk mengeluarkan pesan dengan tipe (efek) tertentu. Dispatcher, atas permintaan take , melihat semua ruang penyimpanan, dan jika data ini tidak ada, maka take tetap dengan dispatcher dan menunggu sampai put membawa data dengan tipe yang diperlukan untuk take . Ada berbagai jenis messenger seperti itu (takeEvery, dll.).





Ide utama ruang penyimpanan adalah untuk "memisahkan" pengirim dan penerima dalam waktu (semacam analog dari pemrosesan asinkron).





Redux-saga hanyalah sebuah alat, tetapi hal utama di sini adalah orang yang mengirim semua utusan ini dan memproses data yang mereka bawa. "Seseorang" ini adalah fungsi generator (saya akan menyebutnya penumpang), yang disebut saga dalam bantuan dan diteruskan saat middleware dimulai . Anda dapat menjalankan middleware dengan dua cara: menggunakan middleware.run (saga, ... args) dan runSaga (options, saga, ... args). Saga adalah fungsi generator dengan logika pemrosesan efek.





Saya tertarik dengan kemungkinan menggunakan redux-saga untuk menangani acara eksternal tanpa redux. Izinkan saya mempertimbangkan metode runSaga (...) secara lebih rinci:





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





  • cara menggunakan redux-saga tanpa redux





  • cara menggunakan redux-saga untuk mengelola status hook









Tautan kotak  pasir





Link  repositori









Bersambung ... RxJS ...












All Articles