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



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

以下是组件:…

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
}

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

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

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对象,以便能够通过useEffect中的ref.current获取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);

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

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中时,才会调用hook。

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

最新更新