Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React setState hook from scroll event listener

Tags:

First of all, with a class component, this works fine and does not cause any issues.

However, in functional component with hooks, whenever I try to set state from my scroll event listener's function handleScroll, my state fails to get updated or app's performance gets affected drastically even though I am using debounce.

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; 

Tried to use useCallback hook in handleScroll function but it did not help much.

What am I doing wrong? How can I set state from handleScroll without a huge impact on performance?

I've created a sandbox with this issue.

Edit React setState from event listener

like image 732
zilijonas Avatar asked Jul 18 '19 07:07

zilijonas


2 Answers

In your code I see several issues:

1) [] in useEffect means it will not see any changes of state, like changes of goingUp. It will always see initial value of goingUp

2) debounce does not work so. It returns a new debounced function.

3) usually global variables is an anti-pattern, thought it works just in your case.

4) your scroll listener is not passive, as mentioned by @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

like image 154
Yozi Avatar answered Nov 18 '22 10:11

Yozi


In short, you need to add goingUp as the dependency of useEffect.

If you use [], you will only create/remove a listener with a function(handleScroll, which is created in the initial render). In other words, when re-render, the scroll event listener is still using the old handleScroll from the initial render.

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

Using custom hooks

I recommend move the whole logic into a custom hooks, which can make your code more clear and easy to reuse. I use useRef to store the previous value.

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'; } 
like image 20
NCM Avatar answered Nov 18 '22 09:11

NCM