自定义hook,滚动加载

218 阅读3分钟

写的可能不够好,希望大佬指点 接受以下参数:

  1. service:一个异步函数,用于获取数据。它接受一个布尔值参数 isReset,用于指示是否重置数据。该函数返回一个 Promise,用于表示异步操作的结果。

  2. container(可选):一个 HTMLDivElement 元素,表示滚动元素的容器。如果不传递此参数,默认使用 body 元素作为滚动容器。

  3. options(可选):一个包含一些配置选项的对象。

    • threshold(可选):一个数字,表示当滚动元素距离底部还有多少像素时触发加载,默认值为 0。
    • afterFetching(可选):一个回调函数,在每次获取数据后执行。它接受一个参数 data,表示获取的数据。
    • isEnd(可选):一个函数,用于判断是否已经加载完所有数据。它接受一个可选参数 allData,表示当前已加载的所有数据,返回一个布尔值。
    • watch(可选):一个用于监视变化的值,当该值发生变化时,会重新加载数据。

该 hook 返回一个包含两个元素的数组:

  1. 第一个元素是数据 data,表示已加载的数据。初始值为空数组。
  2. 第二个元素是一个布尔值 isLoading,表示当前是否正在加载数据。初始值为 false

此外,该 hook 还会根据传入的参数进行一些额外的操作:

  • 在首次加载时,会调用 wrappedService 函数获取数据。
  • 监听 watch 参数的变化,当其发生变化时,会重新加载数据,并将 watchedValue.current 更新为最新的 watch 值。
  • 监听滚动事件,并在满足一定条件时触发加载数据的操作。具体条件为:滚动元素距离底部的距离小于等于 threshold,并且 isEnd 函数返回 false,且当前没有正在加载的数据。
  • 在组件卸载时,会移除滚动事件的监听。

总结来说,该 hook 实现了一个滚动加载的功能,通过监听滚动事件,并在满足一定条件时触发异步请求数据的操作,从而实现了数据的动态加载。

import lodash from "lodash";
import { useCallback, useEffect, useRef, useState } from "react";

export interface Option<Data> {
  threshold?: number;
  afterFetching?: (data: Data) => void;
  isEnd?: (allData?: Data) => boolean;
  watch?: any;
}

// type TData = Record<string, any>[];

/**
 * 滚动加载
 * @param service 请求函数
 * @param container 滚动元素容器,不传默认使用body
 * @param options
 * @returns
 */
/**
 *
 * @description 获取的数据是累积添加的
 */
export function useScrollFetch<T extends any[] | undefined>(
  service: (isReset: boolean) => Promise<T>,
  container?: HTMLDivElement | undefined,
  options?: Option<T>,
): [T | undefined, boolean] {
  const { threshold, afterFetching, isEnd, watch } = {
    threshold: 0,
    ...options,
  };

  if (typeof isEnd !== "function") {
    throw Error("options.isEnd must be a function");
  }

  const [isLoading, toggleLoading] = useState<boolean>(false);
  const [data, setData] = useState<T>([] as unknown as T);
  const watchedValue = useRef<any>(watch);
  const isFetching = useRef<boolean>(false);
  const initialized = useRef<boolean>(false);

  const wrappedService = useCallback(
    (reset?: boolean) => {
      isFetching.current = true;
      toggleLoading(true);
      const promise = service(!!reset);

      if (!(promise instanceof Promise)) {
        console.warn("service must be an async function");
        return Promise.resolve(promise);
      }
      return promise
        .then((response) => {
          if (typeof afterFetching === "function") {
            afterFetching(response);
          }

          isFetching.current = false;

          if (reset) {
            setData(response);
          } else if (response) {
            setData(data?.concat(response) as T);
          }
          toggleLoading(false);
        })
        .catch(() => {
          toggleLoading(false);
        });
    },
    [afterFetching, data, service],
  );

  // 第一次加载
  useEffect(() => {
    if (!initialized.current) {
      wrappedService();

      initialized.current = true;
    }
  }, [wrappedService]);

  // 监听watch变化,初始时不应该触发加载,有变化后,需要重置数据
  useEffect(() => {
    if (typeof watch === "undefined" || watch === null) {
      return;
    }

    const shouldReload = Array.isArray(watchedValue.current)
      ? watchedValue.current.some((item, index) => !lodash?.isEqual(item, watch[index]))
      : watchedValue.current !== watch;

    if (shouldReload) {
      wrappedService(true);
      watchedValue.current = watch;
    }
  }, [watch, wrappedService]);

  const handleOnScroll = useCallback(() => {
    const shouldFetch =
      (!container
        ? document.documentElement.scrollHeight - window.scrollY - document.body.offsetHeight <= threshold
        : container.scrollHeight - container.scrollTop - container.offsetHeight <= threshold) && !isEnd(data);

    if (shouldFetch && !isFetching.current) {
      wrappedService();
    }
  }, [container, data, isEnd, threshold, wrappedService]);

  // 添加事件监听
  useEffect(() => {
    (container || document).addEventListener("scroll", handleOnScroll);
    return () => {
      (container || document).removeEventListener("scroll", handleOnScroll);
    };
  }, [container, handleOnScroll]);

  useEffect(() => {
    if (container) {
      // NOTE: 需要给container设置overflow:scroll才监听的到scroll
      container!.style!.overflow = "scroll";
    }
  }, [container]);

  return [data, isLoading];
}