React自定义钩子滚动侦听器仅触发一次

时间:2020-07-15 19:16:07

标签: javascript reactjs scroll react-hooks

我尝试使用自定义钩子在组件中实现滚动指示器。

以下是组件: ...

const DetailListInfo: React.FC<Props> = props => {
  const container = useRef(null)
  const scrollable = useScroll(container.current)
  const { details } = props

  return (
    <div
      ref={container}
      className="content-list-info content-list-info-detailed"
    >
      {details && renderTypeDetails(details)}
      {scrollable && <ScrollIndicator />}
    </div>
  )
}

export default inject("store")(observer(DetailListInfo))

和useScroll挂钩:

import React, { useState, useEffect } from "react"
import { checkIfScrollable } from "../utils/scrollableElement"

export const useScroll = (container: HTMLElement) => {
  const [isScrollNeeded, setScrollValue] = useState(true)
  const [isScrollable, setScrollable] = useState(false)

  const checkScrollPosition = (): void => {
    const scrollDiv = container
    const result =
      scrollDiv.scrollTop < scrollDiv.scrollHeight - scrollDiv.clientHeight ||
      scrollDiv.scrollTop === 0
    setScrollValue(result)
  }

  useEffect(() => {
    console.log("Hook called")
    if (!container) return null

    container.addEventListener("scroll", checkScrollPosition)
    setScrollable(checkIfScrollable(container))
    return () => container.removeEventListener("scroll", checkScrollPosition)
  }, [isScrollable, isScrollNeeded])

  return isScrollNeeded && isScrollable
}

因此,在此传递的组件中的每个滚动上(容器不同,这就是为什么我要制作可自定义的钩子),我想检查当前滚动位置以有条件地显示或隐藏指示器。问题是,渲染组件时,该挂钩仅被调用一次。它不是在监听滚动事件。 当此挂钩位于组件内部时,它工作正常。怎么了?

2 个答案:

答案 0 :(得分:1)

让我们研究您的代码:

const container = useRef(null)
const scrollable = useScroll(container.current) // initial container.current is null

// useScroll
const useScroll = (container: HTMLElement) => {
  // container === null at the first render
  ...

  // useEffect depends only from isScrollable, isScrollNeeded
  // these variables are changed inside the scroll listener and this hook body
  // but at the first render the container is null so the scroll subscription is not initiated 
  // and hook body won't be executed fully because there's return statement
  useEffect(() => {
    if (!container) return null
    ...
  }, [isScrollable, isScrollNeeded])
}

要使一切正常工作,您的useEffect挂钩应在挂钩主体中使用所有依赖项。请注意文档中的warning notes

此外,您不能仅将ref.current传递给一个钩子。该字段是可变的,并且ref.current更改时(挂载时)钩子将不会得到通知(重新执行)。您应该传递整个ref对象,以便能够通过ref.current内的useEffect获取HTML元素。

此功能的正确版本应如下:

export const useScroll = (ref: React.RefObject<HTMLElement>) => {
  const [isScrollNeeded, setScrollValue] = useState(true);
  const [isScrollable, setScrollable] = useState(false);

  useEffect(() => {
    const container = ref.current;

    if (!container) return;

    const checkScrollPosition = (): void => {
      const scrollDiv = container;
      const result =
        scrollDiv.scrollTop < scrollDiv.scrollHeight - scrollDiv.clientHeight ||
        scrollDiv.scrollTop === 0;
      setScrollValue(result);
      setScrollable(checkIfScrollable(scrollDiv));
    };

    container.addEventListener("scroll", checkScrollPosition);
    setScrollable(checkIfScrollable(container));
    return () => container.removeEventListener("scroll", checkScrollPosition);

    // this is not the best place to depend on isScrollNeeded or isScrollable
    // because every time on these variables are changed scroll subscription will be reinitialized
    // it seems that it is better to do all calculations inside the scroll handler
  }, [ref]);

  return isScrollNeeded && isScrollable
}

// somewhere in a render:
const ref = useRef(null);
const isScrollable = useScroll(ref);

答案 1 :(得分:0)

其中具有滚动侦听器的钩子:

export const ScrollIndicator: React.FC<Props> = props => {
  const { container } = props
  const [isScrollNeeded, setScrollValue] = useState(true)
  const [isScrollable, setScrollable] = useState(false)

  const handleScroll = (): void => {
    const scrollDiv = container
    const result =
      scrollDiv.scrollTop < scrollDiv.scrollHeight - scrollDiv.clientHeight ||
      scrollDiv.scrollTop === 0

    setScrollValue(result)
  }

  useEffect(() => {
    setScrollable(checkIfScrollable(container))
    container.addEventListener("scroll", handleScroll)
    return () => container.removeEventListener("scroll", handleScroll)
  }, [container, handleScroll])

  return isScrollable && isScrollNeeded && <Indicator />
}

在render组件中,需要检查ref容器是否存在。仅当容器已经在DOM中时,才会调用钩子。

const scrollDiv = useRef(null)
{scrollDiv.current && <ScrollIndicator container={scrollDiv.current} />}