【问题标题】:React custom hook scroll listener fired only onceReact 自定义钩子滚动监听器只触发一次
【发布时间】:2020-07-18 04:18:33
【问题描述】:

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

这是组件: ...

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
}

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

【问题讨论】:

    标签: javascript reactjs scroll react-hooks


    【解决方案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.currentuseEffect 中获取 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);
    

    【讨论】:

    • 感谢您的回复。不幸的是,这会炸毁整个应用程序。我收到新错误:An effect function must not return anything besides a function, which is used for clean-up. You returned null. If your effect does not require clean up,destroy is not a function
    • 是的,这是因为 useEffect 不应该返回 null。 IE。而不是if (!container) return null 应该是if (!container) return;
    • 我还注意到另一个问题(关于钩子参数)并扩展了我的答案。
    • 感谢您提供另一个全面的答案。感谢您的提示,我设法找到了解决方案(在我编辑的帖子中)。
    【解决方案2】:

    内部有滚动监听器的钩子:

    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 />
    }
    

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-09-17
      • 1970-01-01
      • 2020-03-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多