React hook not updated in function listened on scroll event

Add isFetching as a dependency to useEffect

While i can't provide a deep explanation, I can say that you basically lied to React in useEffect when you said the effect doesn't depend on anything by providing an empty array of dependencies it's always good to pass all the variables that include in your effect.

Also you create a new function every time the component re-render, to avoid this move the function inside of useEffect or wrap it inside useCallback which will not create re-create the function unless something in the array of dependencies changes

useEffect(
  () => {
    const handleScroll = debounce(() => {
      setIsFetching(prevState => !prevState);
      console.log({ isFetching });
    }, 300);
    window.addEventListener("scroll", handleScroll);
    return () => window.removeEventListener("scroll", handleScroll);
  },
  [isFetching]
);

Or with useCallback

const handleScroll = useCallback(
  debounce(() => {
    setIsFetching(prevState => !prevState);
    console.log({ isFetching });
  }, 300),
  [isFetching]
);

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

complete guide to useEffect


In order to listen the changes in your state from inside the useEffect callback (when you're not following any props update), you can save your state in a variable outside your component's scope, and using it instead of the state directly.

Here you have the code:

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

const debounce = (func, wait, immediate) => {
  let timeout;
  return function() {
    const context = this;
    const args = arguments;
    clearTimeout(timeout);
    timeout = setTimeout(() => {
      timeout = null;
      if (!immediate) func.apply(context, args);
    }, wait);
    if (immediate && !timeout) func.apply(context, args);
  };
};

let isFetchingState;

const App = () => {
  const [isFetching, setIsFetching] = useState(false);

  isFetchingState = isFetching;

  const handleScroll = debounce(() => {
    setIsFetching(!isFetchingState);
    console.log({ isFetchingState });
  }, 300);

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

  return <div style={{ height: "1280px" }}>Hello world</div>;
};

const root = document.getElementById("root");

if (root) ReactDOM.render(<App />, root);