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