检测Reaction js中的滚动方向

siv3szwd  于 2022-09-21  发布在  React
关注(0)|答案(9)|浏览(187)

我正在尝试检测Scroll事件是向上还是向下,但我找不到解决方案。

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中不会。

有什么建议吗?我做错了什么?

谢谢你的帮忙

kkih6yb8

kkih6yb81#

这是因为您定义了一个没有任何依赖项的**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]);

如果由于某种原因窗口可能在那里不可用,或者您不想在这里执行,您可以在两个单独的e1d10d1中执行此操作。

因此,您的e1d11d1应该是这样的:

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()作为依赖项添加到useEffect(),然后由于此更改,我们应该优化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状态将包含实际滚动方向。

工作演示:

8i9zcol2

8i9zcol22#

在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>
 </>
 )
}
mmvthczy

mmvthczy3#

我只是想提出一个整洁的解决方案,它很像哈巴汉,但在我看来看起来更整洁一些。

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状态,即可在代码中随心所欲地执行操作。

mo49yndu

mo49yndu4#

我发现这个简洁而简单的解决方案只需要几行代码

<div onWheel={ event => {
   if (event.nativeEvent.wheelDelta > 0) {
     console.log('scroll up');
   } else {
     console.log('scroll down');
   }
 }}
>
  scroll on me!
</div>

OnWheels合成事件返回一个事件对象,该对象具有一个名为nativeEvent的属性,其中包含原始事件信息。即使没有有效的滚动(overflow:hidden),也可以使用WEELDELTA检测方向。

这是原始源->http://blog.jonathanargentiero.com/detect-scroll-direction-on-react/

bis0qfac

bis0qfac5#

在我看来,大多数答案似乎有点过度设计。

以下是我在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();

    {....}
}
mccptt67

mccptt676#

我环顾四周,找不到一个简单的解决方案,所以我查看了事件本身,发现存在一个“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");
    }
};
brccelvz

brccelvz7#

以下是我的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-intenthttps://github.com/dollarshaveclub/scrolldir。也被移植到这里React:https://github.com/AnakinYuen/scroll-direction

nr9pn0ug

nr9pn0ug8#

以下是我的解决方案,它扩展了这里找到的一些想法。它在每次方向改变时只触发一次,并添加一些参数来微调钩子调用

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

Codepen demo

cbeh67ev

cbeh67ev9#

我已经找了这个东西好几个小时了。但是没有一个解决方案对我有效,所以我这样写,并为我的next.js项目工作。

const [currentScroll, setCurrentScroll] = useState(0)
const [lastScroll, setLastScroll] = useState(0)
const [scrollUp, setScrollUp] = useState(false)

useEffect(()=>{
  function handleScroll(){
    setCurrentScroll(scrollY)

    // check if current scroll 
    // more than last scroll
    if(currentScroll>lastScroll){
      setScrollUp('Down')
    } else {
      setScrollUp('Up')
    }
  }

  // change the last scroll
  setLastScroll(scrollY)

  window.addEventListener('scroll', handleScroll)

  return () => {
   window.removeEventListener('scroll', handleScroll)
  }

// this needed to change last scroll
// if currentscroll has change
},[currentScroll]) }

相关问题