繁体   English   中英

React 自定义钩子滚动监听器只触发一次

[英]React custom hook scroll listener fired only once

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

这是组件:...

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
}

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

让我们研究一下您的代码:

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钩子应该在钩子体内使用所有依赖项。 请注意文档中的警告说明

您也不能只将ref.current传递给钩子。 该字段是可变的,并且当ref.current更改(挂载时)时,您的钩子将不会被通知(重新执行)。 您应该通过整个ref object 以便能够通过ref.current中的useEffect获得 HTML 元素。

此 function 的正确版本应如下所示:

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);

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM