簡體   English   中英

從滾動事件偵聽器反應 setState 鈎子

[英]React setState hook from scroll event listener

首先,對於類組件,這可以正常工作並且不會導致任何問題。

但是,在帶有鈎子的功能組件中,每當我嘗試從滾動事件偵聽器的函數handleScroll設置狀態時,即使我使用了去抖動,我的狀態也無法更新或應用程序的性能會受到嚴重影響。

import React, { useState, useEffect } from "react";
import debounce from "debounce";

let prevScrollY = 0;

const App = () => {
  const [goingUp, setGoingUp] = useState(false);

  const handleScroll = () => {
    const currentScrollY = window.scrollY;
    if (prevScrollY < currentScrollY && goingUp) {
      debounce(() => {
        setGoingUp(false);
      }, 1000);
    }
    if (prevScrollY > currentScrollY && !goingUp) {
      debounce(() => {
        setGoingUp(true);
      }, 1000);
    }

    prevScrollY = currentScrollY;
    console.log(goingUp, currentScrollY);
  };

  useEffect(() => {
    window.addEventListener("scroll", handleScroll);
    return () => window.removeEventListener("scroll", handleScroll);
  }, []);

  return (
    <div>
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
    </div>
  );
};

export default App;

嘗試在handleScroll函數中使用useCallback鈎子,但沒有太大幫助。

我究竟做錯了什么? 如何在不影響性能的情況下從handleScroll設置狀態?

我已經用這個問題創建了一個沙箱。

從事件偵聽器編輯 React setState

在您的代碼中,我看到了幾個問題:

1) useEffect 中的[]意味着它不會看到任何狀態變化, goingUp變化。 它總是會看到goingUp初始值

2) debounce不起作用。 它返回一個新的去抖動函數。

3)通常全局變量是一種反模式,認為它只適用於您的情況。

4)你的滾動監聽器不是被動的,正如@skyboyer 所提到的。

import React, { useState, useEffect, useRef } from "react";

const App = () => {
  const prevScrollY = useRef(0);

  const [goingUp, setGoingUp] = useState(false);

  useEffect(() => {
    const handleScroll = () => {
      const currentScrollY = window.scrollY;
      if (prevScrollY.current < currentScrollY && goingUp) {
        setGoingUp(false);
      }
      if (prevScrollY.current > currentScrollY && !goingUp) {
        setGoingUp(true);
      }

      prevScrollY.current = currentScrollY;
      console.log(goingUp, currentScrollY);
    };

    window.addEventListener("scroll", handleScroll, { passive: true });

    return () => window.removeEventListener("scroll", handleScroll);
  }, [goingUp]);

  return (
    <div>
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
    </div>
  );
};

export default App;

https://codesandbox.io/s/react-setstate-from-event-listener-q7to8

總之,需要添加goingUp作為goingUp的依賴。

如果您使用[] ,您將只使用函數( handleScroll ,在初始渲染中創建)創建/刪除偵聽器。 換句話說,重新渲染時,滾動事件偵聽器仍在使用初始渲染時的舊handleScroll

  useEffect(() => {
    window.addEventListener("scroll", handleScroll);
    return () => window.removeEventListener("scroll", handleScroll);
  }, [goingUp]);

使用自定義鈎子

我建議將整個邏輯移動到自定義鈎子中,這可以使您的代碼更清晰,更易於重用。 我使用useRef來存儲以前的值。

export function useScrollDirection() {
  const prevScrollY = useRef(0)
  const [goingUp, setGoingUp] = useState(false);

  const handleScroll = () => {
    const currentScrollY = window.scrollY;
    if (prevScrollY.current < currentScrollY && goingUp) {
      setGoingUp(false);
    }
    if (prevScrollY.current > currentScrollY && !goingUp) {
      setGoingUp(true);
    }
    prevScrollY.current = currentScrollY;
  };

  useEffect(() => {
    window.addEventListener("scroll", handleScroll);
    return () => window.removeEventListener("scroll", handleScroll);
  }, [goingUp]); 
  return goingUp ? 'up' : 'down';
}

您正在每個渲染上重新創建handleScroll函數,因此它是指實際數據( goingup ),因此您在這里不需要useCallback

但是除了你重新創建handleScroll你被設置為事件監聽器只有它的第一個實例 - 因為你給useEffect(..., [])它只在安裝時運行一次。

一種解決方案是使用最新的handleScroll重新訂閱scroll 要做到這一點,你必須在useEffect返回清理函數(現在它返回一個更多的訂閱)並刪除[]以在每次渲染時運行它:

useEffect(() => {
  window.addEventListener("scroll", handleScroll);
  return () => window.removeEventListener("scroll", handleScroll);
});

PS,您最好使用被動事件偵聽器進行滾動。

這是我對這種情況的解決方案

const [pageUp, setPageUp] = useState(false)
const lastScroll = useRef(0)

const checkScrollTop = () => {
  const currentScroll = window.pageYOffset
  setPageUp(lastScroll.current > currentScroll)
  lastScroll.current = currentScroll
}

// lodash throttle function
const throttledFunc = throttle(checkScrollTop, 400, { leading: true })

useEffect(() => {
  window.addEventListener("scroll", throttledFunc, false)
  return () => {
    window.removeEventListener("scroll", throttledFunc)
  }
}, [])

僅在componentDidMount時添加事件偵聽器應該是一次。 並非在pageUp引起的所有更改中。 所以,我們不需要添加pageUpuseEffect依賴項。

滾動時編輯切換

Hint:您可以添加throttledFuncuseEffect如果它的變化,你需要重新呈現組件

const objDiv = document.getElementById('chat_body');
objDiv.scrollTop = objDiv.scrollHeight;

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM