在 React js 中检测滚动方向
Detect scroll direction in React js
我正在尝试检测滚动事件是向上还是向下,但我找不到解决方案。
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
const Navbar = ({ className }) => {
const [y, setY] = useState(0);
const handleNavigation = (e) => {
const window = e.currentTarget;
if (y > window.scrollY) {
console.log("scrolling up");
} else if (y < window.scrollY) {
console.log("scrolling down");
}
setY(window.scrollY);
};
useEffect(() => {
setY(window.scrollY);
window.addEventListener("scroll", (e) => handleNavigation(e));
}, []);
return (
<nav className={className}>
<p>
<i className="fas fa-pizza-slice"></i>Food finder
</p>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
</ul>
</nav>
);
};
export default Navbar;
基本上它总是被检测为“关闭”,因为 handleNavigation
中的 y
始终为 0。如果我检查 DevTool 中的状态,y
状态更新但在 handleNavigation
没有。
有什么建议我做错了什么吗?
感谢您的帮助
这是因为你定义了一个没有任何依赖的useEffect()
,所以你的useEffect()
只会运行一次,并且它从不在 y
更改 上调用 handleNavigation()
。要解决此问题,您需要将 y
添加到依赖项数组中,以便在 y
值发生变化时告诉您的 useEffect()
运行。然后您需要另一个更改才能在您的代码中生效,您正在尝试使用 window.scrollY
初始化 y
,因此您应该在 useState()
中执行此操作,例如:
const [y, setY] = useState(window.scrollY);
useEffect(() => {
window.addEventListener("scroll", (e) => handleNavigation(e));
return () => { // return a cleanup function to unregister our function since its gonna run multiple times
window.removeEventListener("scroll", (e) => handleNavigation(e));
};
}, [y]);
如果由于某种原因 window 在那里不可用或者您不想在这里使用,您可以在两个单独的 useEffect()
中使用。
所以你的 useEffect()
应该是这样的:
useEffect(() => {
setY(window.scrollY);
}, []);
useEffect(() => {
window.addEventListener("scroll", (e) => handleNavigation(e));
return () => { // return a cleanup function to unregister our function since its gonna run multiple times
window.removeEventListener("scroll", (e) => handleNavigation(e));
};
}, [y]);
更新(工作解决方案)
在我自己实现这个解决方案之后。我发现有一些注释应该应用于此解决方案。所以 因为 handleNavigation()
会直接改变 y
值,我们可以忽略 y
作为我们的依赖,然后添加 handleNavigation()
作为我们的 [=14] 的依赖=],那么由于这个变化我们应该优化handleNavigation()
,所以我们应该使用useCallback()
。那么最后的结果会是这样的:
const [y, setY] = useState(window.scrollY);
const handleNavigation = useCallback(
e => {
const window = e.currentTarget;
if (y > window.scrollY) {
console.log("scrolling up");
} else if (y < window.scrollY) {
console.log("scrolling down");
}
setY(window.scrollY);
}, [y]
);
useEffect(() => {
setY(window.scrollY);
window.addEventListener("scroll", handleNavigation);
return () => {
window.removeEventListener("scroll", handleNavigation);
};
}, [handleNavigation]);
在@RezaSam 发表评论后,我注意到我在记忆版本中犯了一个微小的错误。在另一个箭头函数中调用 handleNavigation
的地方,我发现(通过浏览器开发工具,事件侦听器选项卡)在每个组件重新渲染时它会向 window
注册一个新事件,因此它可能会破坏整个搞事情。
工作演示:
最终优化方案
毕竟,我最终认为 memoization 在这种情况下将帮助我们注册单个事件,以识别滚动方向,但它在打印控制台时并未完全优化,因为我们在handleNavigation
函数,并且没有其他方法可以在当前实现中打印所需的控制台。
所以,我意识到每次我们想要检查新位置时,有一种更好的方法来存储最后一页滚动位置。此外,为了摆脱大量的安慰 向上滚动 和 向下滚动 ,我们应该定义一个阈值 (使用去抖动方法) 来触发滚动事件变化。所以我只是在网上搜索了一下,最后得到了这个非常有用的 gist。然后在它的启发下,实现了一个更简单的版本。
这是它的样子:
const [scrollDir, setScrollDir] = useState("scrolling down");
useEffect(() => {
const threshold = 0;
let lastScrollY = window.pageYOffset;
let ticking = false;
const updateScrollDir = () => {
const scrollY = window.pageYOffset;
if (Math.abs(scrollY - lastScrollY) < threshold) {
ticking = false;
return;
}
setScrollDir(scrollY > lastScrollY ? "scrolling down" : "scrolling up");
lastScrollY = scrollY > 0 ? scrollY : 0;
ticking = false;
};
const onScroll = () => {
if (!ticking) {
window.requestAnimationFrame(updateScrollDir);
ticking = true;
}
};
window.addEventListener("scroll", onScroll);
console.log(scrollDir);
return () => window.removeEventListener("scroll", onScroll);
}, [scrollDir]);
它是如何工作的?
我会简单地从上到下解释每个代码块。
所以我刚刚定义了一个初始值为0
的阈值点,然后每当滚动向上或向下时,它都会进行新的计算,如果你不这样做,你可以增加它想立即计算新的页面偏移量。
然后我决定使用 pageYOffset
而不是使用 scrollY
,这在交叉浏览中更可靠。
在updateScrollDir
函数中,我们将简单地检查是否满足阈值,如果满足,我将根据当前页面和上一页偏移指定滚动方向。
其中最重要的部分是onScroll
函数。我只是使用 requestAnimationFrame
来确保我们在滚动后页面完全呈现后计算新的偏移量。然后使用 ticking
标志,我们将确保在每个 requestAnimationFrame
.
中我们只是 运行 我们的事件监听器回调一次
最后,我们定义了监听器和清理函数。
然后 scrollDir
状态将包含实际滚动方向。
工作演示:
我环顾四周,找不到简单的解决方案,所以我调查了事件本身,发现存在一个“deltaY”,它使一切变得更简单(无需保留最后滚动值的状态)。 “deltaY”值显示事件在“y”中的变化(正 deltaY 表示它是向下滚动事件,负 deltaY 表示它是向上滚动事件)。
工作原理如下:
componentDidMount() {
window.addEventListener('scroll', e => this.handleNavigation(e));
}
handleNavigation = (e) => {
if (e.deltaY > 0) {
console.log("scrolling down");
} else if (e.deltaY < 0) {
console.log("scrolling up");
}
};
这是我的 React 钩子解决方案,useScrollDirection
:
import { useEffect, useState } from 'react'
export type ScrollDirection = '' | 'up' | 'down'
type HistoryItem = { y: number; t: number }
const historyLength = 32 // Ticks to keep in history.
const historyMaxAge = 512 // History data time-to-live (ms).
const thresholdPixels = 64 // Ignore moves smaller than this.
let lastEvent: Event
let frameRequested: Boolean = false
let history: HistoryItem[] = Array(historyLength)
let pivot: HistoryItem = { t: 0, y: 0 }
export function useScrollDirection({
scrollingElement,
}: { scrollingElement?: HTMLElement | null } = {}): ScrollDirection {
const [scrollDirection, setScrollDirection] = useState<ScrollDirection>('')
useEffect(() => {
const element: Element | null =
scrollingElement !== undefined ? scrollingElement : document.scrollingElement
if (!element) return
const tick = () => {
if (!lastEvent) return
frameRequested = false
let y = element.scrollTop
const t = lastEvent.timeStamp
const furthest = scrollDirection === 'down' ? Math.max : Math.min
// Apply bounds to handle rubber banding
const yMax = element.scrollHeight - element.clientHeight
y = Math.max(0, y)
y = Math.min(yMax, y)
// Update history
history.unshift({ t, y })
history.pop()
// Are we continuing in the same direction?
if (y === furthest(pivot.y, y)) {
// Update "high-water mark" for current direction
pivot = { t, y }
return
}
// else we have backed off high-water mark
// Apply max age to find current reference point
const cutoffTime = t - historyMaxAge
if (cutoffTime > pivot.t) {
pivot.y = y
history.filter(Boolean).forEach(({ y, t }) => {
if (t > cutoffTime) pivot.y = furthest(pivot.y, y)
})
}
// Have we exceeded threshold?
if (Math.abs(y - pivot.y) > thresholdPixels) {
pivot = { t, y }
setScrollDirection(scrollDirection === 'down' ? 'up' : 'down')
}
}
const onScroll = (event: Event) => {
lastEvent = event
if (!frameRequested) {
requestAnimationFrame(tick)
frameRequested = true
}
}
element.addEventListener('scroll', onScroll)
return () => element.removeEventListener('scroll', onScroll)
}, [scrollDirection, scrollingElement])
return scrollDirection
}
用法:
const [scrollingElement, setScrollingElement] = useState<HTMLElement | null>(null)
const ref = useCallback(node => setScrollingElement(node), [setScrollingElement])
const scrollDirection = useScrollDirection({ scrollingElement })
<ScrollingContainer {...{ ref }}>
<Header {...{ scrollDirection }}>
</ScrollingContainer>
基于https://github.com/pwfisher/scroll-intent and https://github.com/dollarshaveclub/scrolldir. Also ported to React here: https://github.com/AnakinYuen/scroll-direction。
我发现这个简单明了的解决方案只需要几行代码
<div onWheel={ event => {
if (event.nativeEvent.wheelDelta > 0) {
console.log('scroll up');
} else {
console.log('scroll down');
}
}}
>
scroll on me!
</div>
onWheel synthetic event returns 具有名为 nativeEvent
的属性的事件对象,其中包含原始事件信息。 wheelDelta用于即使没有有效滚动也能检测方向(overflow:hidden
).
这是原始来源 -> http://blog.jonathanargentiero.com/detect-scroll-direction-on-react/
在我看来,大多数答案似乎都设计过度了。
这是我在我的 nextjs 项目中使用的:
function useVerticalScrollDirection() {
const [direction, setDirection] = useState('up');
let prevScrollY = 0;
useEffect(() => {
// Using lodash, we set a throttle to the scroll event
// making it not fire more than once every 500 ms.
window.onscroll = throttle(() => {
// This value keeps the latest scrollY position
const { scrollY } = window;
// Checks if previous scrollY is less than latest scrollY
// If true, we are scrolling downwards, else scrollig upwards
const direction = prevScrollY < scrollY ? 'down' : 'up';
// Updates the previous scroll variable AFTER the direction is set.
// The order of events is key to making this work, as assigning
// the previous scroll before checking the direction will result
// in the direction always being 'up'.
prevScrollY = scrollY;
// Set the state to trigger re-rendering
setDirection(direction);
}, 500);
return () => {
// Remove scroll event on unmount
window.onscroll = null;
};
}, []);
return direction;
}
然后我像这样使用我的组件:
function MyComponent() {
const verticalScrollDirection = useVerticalScrollDirection();
{....}
}
在Next.js试试这个(如果你正在挣扎)-
我用过这个包 - react-use-scroll-direction
import React from 'react'
import { useScrollDirection } from 'react-use-scroll-direction'
export const Window_Scroll_Direction = () => {
const [direction, setDirection] = React.useState(String)
const { isScrollingUp, isScrollingDown } = useScrollDirection()
React.useEffect(() => {
isScrollingDown && setDirection('down')
isScrollingUp && setDirection('up')
}, [isScrollingDown, isScrollingUp])
return (
<>
<div className="fixed top-0 bg-white">
{direction === 'down' ? 'Scrolling down' : 'scrolling up'}
</div>
</>
)
}
只是想提出一个简洁的解决方案,它与 habbahans 非常相似,但在我看来看起来更简洁一些。
let oldScrollY = 0;
const [direction, setDirection] = useState('up');
const controlDirection = () => {
if(window.scrollY > oldScrollY) {
setDirection('down');
} else {
setDirection('up');
}
oldScrollY = window.scrollY;
}
useEffect(() => {
window.addEventListener('scroll', controlDirection);
return () => {
window.removeEventListener('scroll', controlDirection);
};
},[]);
在这里,您只需访问 hidden
状态即可在代码中执行您希望的操作。
这是我的解决方案,它扩展了此处找到的一些想法。它只在每个方向改变时触发一次,并添加一些参数来微调挂钩调用
const useScrollDirection = ({
ref,
threshold,
debounce,
scrollHeightThreshold,
}) => {
threshold = threshold || 10;
debounce = debounce || 10;
scrollHeightThreshold = scrollHeightThreshold || 0;
const [scrollDir, setScrollDir] = useState(null);
const debouncedSetScrollDir = _.debounce(setScrollDir, debounce);
useEffect(() => {
let lastScrollY = ref?.current?.scrollTop;
let lastScrollDir;
let ticking = false;
const hasScrollHeightThreshold =
ref?.current?.scrollHeight - ref?.current?.clientHeight >
scrollHeightThreshold;
const updateScrollDir = () => {
const scrollY = ref?.current?.scrollTop;
if (
Math.abs(scrollY - lastScrollY) < threshold ||
!hasScrollHeightThreshold
) {
ticking = false;
return;
}
const newScroll = scrollY > lastScrollY ? 'down' : 'up';
if (newScroll !== lastScrollDir) {
debouncedSetScrollDir(newScroll);
}
lastScrollY = scrollY > 0 ? scrollY : 0;
lastScrollDir = newScroll;
ticking = false;
};
const onScroll = () => {
if (!ticking) {
window.requestAnimationFrame(updateScrollDir);
ticking = true;
}
};
ref?.current?.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, []);
return scrollDir;
};
我正在尝试检测滚动事件是向上还是向下,但我找不到解决方案。
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
const Navbar = ({ className }) => {
const [y, setY] = useState(0);
const handleNavigation = (e) => {
const window = e.currentTarget;
if (y > window.scrollY) {
console.log("scrolling up");
} else if (y < window.scrollY) {
console.log("scrolling down");
}
setY(window.scrollY);
};
useEffect(() => {
setY(window.scrollY);
window.addEventListener("scroll", (e) => handleNavigation(e));
}, []);
return (
<nav className={className}>
<p>
<i className="fas fa-pizza-slice"></i>Food finder
</p>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
</ul>
</nav>
);
};
export default Navbar;
基本上它总是被检测为“关闭”,因为 handleNavigation
中的 y
始终为 0。如果我检查 DevTool 中的状态,y
状态更新但在 handleNavigation
没有。
有什么建议我做错了什么吗?
感谢您的帮助
这是因为你定义了一个没有任何依赖的useEffect()
,所以你的useEffect()
只会运行一次,并且它从不在 y
更改 上调用 handleNavigation()
。要解决此问题,您需要将 y
添加到依赖项数组中,以便在 y
值发生变化时告诉您的 useEffect()
运行。然后您需要另一个更改才能在您的代码中生效,您正在尝试使用 window.scrollY
初始化 y
,因此您应该在 useState()
中执行此操作,例如:
const [y, setY] = useState(window.scrollY);
useEffect(() => {
window.addEventListener("scroll", (e) => handleNavigation(e));
return () => { // return a cleanup function to unregister our function since its gonna run multiple times
window.removeEventListener("scroll", (e) => handleNavigation(e));
};
}, [y]);
如果由于某种原因 window 在那里不可用或者您不想在这里使用,您可以在两个单独的 useEffect()
中使用。
所以你的 useEffect()
应该是这样的:
useEffect(() => {
setY(window.scrollY);
}, []);
useEffect(() => {
window.addEventListener("scroll", (e) => handleNavigation(e));
return () => { // return a cleanup function to unregister our function since its gonna run multiple times
window.removeEventListener("scroll", (e) => handleNavigation(e));
};
}, [y]);
更新(工作解决方案)
在我自己实现这个解决方案之后。我发现有一些注释应该应用于此解决方案。所以 因为 handleNavigation()
会直接改变 y
值,我们可以忽略 y
作为我们的依赖,然后添加 handleNavigation()
作为我们的 [=14] 的依赖=],那么由于这个变化我们应该优化handleNavigation()
,所以我们应该使用useCallback()
。那么最后的结果会是这样的:
const [y, setY] = useState(window.scrollY);
const handleNavigation = useCallback(
e => {
const window = e.currentTarget;
if (y > window.scrollY) {
console.log("scrolling up");
} else if (y < window.scrollY) {
console.log("scrolling down");
}
setY(window.scrollY);
}, [y]
);
useEffect(() => {
setY(window.scrollY);
window.addEventListener("scroll", handleNavigation);
return () => {
window.removeEventListener("scroll", handleNavigation);
};
}, [handleNavigation]);
在@RezaSam 发表评论后,我注意到我在记忆版本中犯了一个微小的错误。在另一个箭头函数中调用 handleNavigation
的地方,我发现(通过浏览器开发工具,事件侦听器选项卡)在每个组件重新渲染时它会向 window
注册一个新事件,因此它可能会破坏整个搞事情。
工作演示:
最终优化方案
毕竟,我最终认为 memoization 在这种情况下将帮助我们注册单个事件,以识别滚动方向,但它在打印控制台时并未完全优化,因为我们在handleNavigation
函数,并且没有其他方法可以在当前实现中打印所需的控制台。
所以,我意识到每次我们想要检查新位置时,有一种更好的方法来存储最后一页滚动位置。此外,为了摆脱大量的安慰 向上滚动 和 向下滚动 ,我们应该定义一个阈值 (使用去抖动方法) 来触发滚动事件变化。所以我只是在网上搜索了一下,最后得到了这个非常有用的 gist。然后在它的启发下,实现了一个更简单的版本。
这是它的样子:
const [scrollDir, setScrollDir] = useState("scrolling down");
useEffect(() => {
const threshold = 0;
let lastScrollY = window.pageYOffset;
let ticking = false;
const updateScrollDir = () => {
const scrollY = window.pageYOffset;
if (Math.abs(scrollY - lastScrollY) < threshold) {
ticking = false;
return;
}
setScrollDir(scrollY > lastScrollY ? "scrolling down" : "scrolling up");
lastScrollY = scrollY > 0 ? scrollY : 0;
ticking = false;
};
const onScroll = () => {
if (!ticking) {
window.requestAnimationFrame(updateScrollDir);
ticking = true;
}
};
window.addEventListener("scroll", onScroll);
console.log(scrollDir);
return () => window.removeEventListener("scroll", onScroll);
}, [scrollDir]);
它是如何工作的?
我会简单地从上到下解释每个代码块。
所以我刚刚定义了一个初始值为
0
的阈值点,然后每当滚动向上或向下时,它都会进行新的计算,如果你不这样做,你可以增加它想立即计算新的页面偏移量。然后我决定使用
pageYOffset
而不是使用scrollY
,这在交叉浏览中更可靠。在
updateScrollDir
函数中,我们将简单地检查是否满足阈值,如果满足,我将根据当前页面和上一页偏移指定滚动方向。其中最重要的部分是
中我们只是 运行 我们的事件监听器回调一次onScroll
函数。我只是使用requestAnimationFrame
来确保我们在滚动后页面完全呈现后计算新的偏移量。然后使用ticking
标志,我们将确保在每个requestAnimationFrame
.最后,我们定义了监听器和清理函数。
然后
scrollDir
状态将包含实际滚动方向。
工作演示:
我环顾四周,找不到简单的解决方案,所以我调查了事件本身,发现存在一个“deltaY”,它使一切变得更简单(无需保留最后滚动值的状态)。 “deltaY”值显示事件在“y”中的变化(正 deltaY 表示它是向下滚动事件,负 deltaY 表示它是向上滚动事件)。
工作原理如下:
componentDidMount() {
window.addEventListener('scroll', e => this.handleNavigation(e));
}
handleNavigation = (e) => {
if (e.deltaY > 0) {
console.log("scrolling down");
} else if (e.deltaY < 0) {
console.log("scrolling up");
}
};
这是我的 React 钩子解决方案,useScrollDirection
:
import { useEffect, useState } from 'react'
export type ScrollDirection = '' | 'up' | 'down'
type HistoryItem = { y: number; t: number }
const historyLength = 32 // Ticks to keep in history.
const historyMaxAge = 512 // History data time-to-live (ms).
const thresholdPixels = 64 // Ignore moves smaller than this.
let lastEvent: Event
let frameRequested: Boolean = false
let history: HistoryItem[] = Array(historyLength)
let pivot: HistoryItem = { t: 0, y: 0 }
export function useScrollDirection({
scrollingElement,
}: { scrollingElement?: HTMLElement | null } = {}): ScrollDirection {
const [scrollDirection, setScrollDirection] = useState<ScrollDirection>('')
useEffect(() => {
const element: Element | null =
scrollingElement !== undefined ? scrollingElement : document.scrollingElement
if (!element) return
const tick = () => {
if (!lastEvent) return
frameRequested = false
let y = element.scrollTop
const t = lastEvent.timeStamp
const furthest = scrollDirection === 'down' ? Math.max : Math.min
// Apply bounds to handle rubber banding
const yMax = element.scrollHeight - element.clientHeight
y = Math.max(0, y)
y = Math.min(yMax, y)
// Update history
history.unshift({ t, y })
history.pop()
// Are we continuing in the same direction?
if (y === furthest(pivot.y, y)) {
// Update "high-water mark" for current direction
pivot = { t, y }
return
}
// else we have backed off high-water mark
// Apply max age to find current reference point
const cutoffTime = t - historyMaxAge
if (cutoffTime > pivot.t) {
pivot.y = y
history.filter(Boolean).forEach(({ y, t }) => {
if (t > cutoffTime) pivot.y = furthest(pivot.y, y)
})
}
// Have we exceeded threshold?
if (Math.abs(y - pivot.y) > thresholdPixels) {
pivot = { t, y }
setScrollDirection(scrollDirection === 'down' ? 'up' : 'down')
}
}
const onScroll = (event: Event) => {
lastEvent = event
if (!frameRequested) {
requestAnimationFrame(tick)
frameRequested = true
}
}
element.addEventListener('scroll', onScroll)
return () => element.removeEventListener('scroll', onScroll)
}, [scrollDirection, scrollingElement])
return scrollDirection
}
用法:
const [scrollingElement, setScrollingElement] = useState<HTMLElement | null>(null)
const ref = useCallback(node => setScrollingElement(node), [setScrollingElement])
const scrollDirection = useScrollDirection({ scrollingElement })
<ScrollingContainer {...{ ref }}>
<Header {...{ scrollDirection }}>
</ScrollingContainer>
基于https://github.com/pwfisher/scroll-intent and https://github.com/dollarshaveclub/scrolldir. Also ported to React here: https://github.com/AnakinYuen/scroll-direction。
我发现这个简单明了的解决方案只需要几行代码
<div onWheel={ event => {
if (event.nativeEvent.wheelDelta > 0) {
console.log('scroll up');
} else {
console.log('scroll down');
}
}}
>
scroll on me!
</div>
onWheel synthetic event returns 具有名为 nativeEvent
的属性的事件对象,其中包含原始事件信息。 wheelDelta用于即使没有有效滚动也能检测方向(overflow:hidden
).
这是原始来源 -> http://blog.jonathanargentiero.com/detect-scroll-direction-on-react/
在我看来,大多数答案似乎都设计过度了。
这是我在我的 nextjs 项目中使用的:
function useVerticalScrollDirection() {
const [direction, setDirection] = useState('up');
let prevScrollY = 0;
useEffect(() => {
// Using lodash, we set a throttle to the scroll event
// making it not fire more than once every 500 ms.
window.onscroll = throttle(() => {
// This value keeps the latest scrollY position
const { scrollY } = window;
// Checks if previous scrollY is less than latest scrollY
// If true, we are scrolling downwards, else scrollig upwards
const direction = prevScrollY < scrollY ? 'down' : 'up';
// Updates the previous scroll variable AFTER the direction is set.
// The order of events is key to making this work, as assigning
// the previous scroll before checking the direction will result
// in the direction always being 'up'.
prevScrollY = scrollY;
// Set the state to trigger re-rendering
setDirection(direction);
}, 500);
return () => {
// Remove scroll event on unmount
window.onscroll = null;
};
}, []);
return direction;
}
然后我像这样使用我的组件:
function MyComponent() {
const verticalScrollDirection = useVerticalScrollDirection();
{....}
}
在Next.js试试这个(如果你正在挣扎)-
我用过这个包 - react-use-scroll-direction
import React from 'react'
import { useScrollDirection } from 'react-use-scroll-direction'
export const Window_Scroll_Direction = () => {
const [direction, setDirection] = React.useState(String)
const { isScrollingUp, isScrollingDown } = useScrollDirection()
React.useEffect(() => {
isScrollingDown && setDirection('down')
isScrollingUp && setDirection('up')
}, [isScrollingDown, isScrollingUp])
return (
<>
<div className="fixed top-0 bg-white">
{direction === 'down' ? 'Scrolling down' : 'scrolling up'}
</div>
</>
)
}
只是想提出一个简洁的解决方案,它与 habbahans 非常相似,但在我看来看起来更简洁一些。
let oldScrollY = 0;
const [direction, setDirection] = useState('up');
const controlDirection = () => {
if(window.scrollY > oldScrollY) {
setDirection('down');
} else {
setDirection('up');
}
oldScrollY = window.scrollY;
}
useEffect(() => {
window.addEventListener('scroll', controlDirection);
return () => {
window.removeEventListener('scroll', controlDirection);
};
},[]);
在这里,您只需访问 hidden
状态即可在代码中执行您希望的操作。
这是我的解决方案,它扩展了此处找到的一些想法。它只在每个方向改变时触发一次,并添加一些参数来微调挂钩调用
const useScrollDirection = ({
ref,
threshold,
debounce,
scrollHeightThreshold,
}) => {
threshold = threshold || 10;
debounce = debounce || 10;
scrollHeightThreshold = scrollHeightThreshold || 0;
const [scrollDir, setScrollDir] = useState(null);
const debouncedSetScrollDir = _.debounce(setScrollDir, debounce);
useEffect(() => {
let lastScrollY = ref?.current?.scrollTop;
let lastScrollDir;
let ticking = false;
const hasScrollHeightThreshold =
ref?.current?.scrollHeight - ref?.current?.clientHeight >
scrollHeightThreshold;
const updateScrollDir = () => {
const scrollY = ref?.current?.scrollTop;
if (
Math.abs(scrollY - lastScrollY) < threshold ||
!hasScrollHeightThreshold
) {
ticking = false;
return;
}
const newScroll = scrollY > lastScrollY ? 'down' : 'up';
if (newScroll !== lastScrollDir) {
debouncedSetScrollDir(newScroll);
}
lastScrollY = scrollY > 0 ? scrollY : 0;
lastScrollDir = newScroll;
ticking = false;
};
const onScroll = () => {
if (!ticking) {
window.requestAnimationFrame(updateScrollDir);
ticking = true;
}
};
ref?.current?.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, []);
return scrollDir;
};