skip to Main Content

I was trying to make an element position : "fixed" after it scrolls to the end of the screen and whenever it comes back to the first screen it should again get the position as "relative" , but state is changing to true (fixed) when it scrolls to the end of the screen but when it scroll back it is not changing to false (relative).

const [fixed, setFixed] = useState(() => {
    return false;
  });
const scrollHandler = () => {
    if (window.scrollY > window.innerHeight) {

      if (!fixed) {
       
        setFixed(true);
      }
    } else {
      if (fixed) {
        setFixed(false);
      }
    }
  };
  useEffect(() => {
    window.addEventListener("scroll", scrollHandler);

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

return (
    <>
      <div
        style={{ position: `${fixed ? "fixed" : "relative"}` }}
        className={classes.navigationBelt}
      ></div>
 </> );

Here is the code explaination
I have taken a state named as "fixed" and initial Value as "false"

const [fixed, setFixed] = useState(() => {
    return false;
  });

I have added an eventListener on the window for the scroll, when the component mounts into the dom.

 useEffect(() => {
    window.addEventListener("scroll", scrollHandler);

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

For Handling the scroll event I have created a function named as "scrollHandler"

const scrollHandler = () => {
    if (window.scrollY > window.innerHeight) {

      if (!fixed) {
       
        setFixed(true);
      }
    } else {
      if (fixed) {
        setFixed(false);
      }
    }
  };

I am setting the "fixed" to "true" when the scroll is more than the height of the window and making an element to "fixed". Note : I am only updating the state when "fixed" state is false, otherwise it will cause multiple re-render.

return (
    <>
      <div
        style={{ position: `${fixed ? "fixed" : "relative"}` }}
        className={classes.navigationBelt}
      ></div>
 </> );

It is working fine, but when it scrolls back and "else" condition hit still the state ("fixed") is showing be false even though it has changed and element has already got fixed. Because of that setFixed(false) does not work as the outer condition is still false.

else {
      if (fixed) {
        setFixed(false);
      }
    }

I wanted to know why is this happening?

2

Answers


  1. Because the scrollHandler you attach to the window has access to the fixed variable when it is false (the initial state).

    You attach it in a useEffect that is only run once, so it will always keep that reference of the scrollHandler.

    You can pass the fixed as a dependency to the useEffect and this way it will refresh/update the event handler.

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

    The above solution has the "problem" that you need to know what scrollHandler does in order to specify/update the dependency list of the useEffect.

    A more sane approach would be to re-run the useEffect when the scrollHandler changes since it is used inside the useEffect (and this is the real dependency of that useEffect)

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

    But since your code will change the scrollHandler handler in each re-render of the component, you should try to make that as "stable" as possible, that is to keep/re-use the same reference for that function while its own dependencies are the same. To do that you can use the useCallback and use the fixed as its dependency.

    const scrollHandler = useCallback(() => {
      if (window.scrollY > window.innerHeight) {
    
        if (!fixed) {
          setFixed(true);
        }
      } else {
        if (fixed) {
          setFixed(false);
        }
      }
    }, [fixed]);
    
    Login or Signup to reply.
  2. Im not really familiar with react but im wondering if there any problems to do it with a additional ref? against the fact that it is not so elegant, but can also be nice if the useEffect not triggered on page reRender?

    export default function Home() {
      const [fixed, setFixed] = useState(false);
      const myFixedRef = useRef(fixed);
      const listener = () => {
        if (window.scrollY > window.innerHeight) {
          if (!myFixedRef.current) {
            setFixed((myFixedRef.current = true));
          }
        } else {
          if (myFixedRef.current) {
            setFixed((myFixedRef.current = false));
          }
        }
      };
    
      useEffect(() => {
        window.addEventListener("scroll", listener);
        return () => {
          window.removeEventListener("scroll", listener);
        };
      }, []);
    

    With this way, i can also get the right way the initial state.

     useEffect(() => {
        listener(); // to get the initial state
        window.addEventListener("scroll", listener);
        return () => {
          window.removeEventListener("scroll", listener);
        };
      }, []);
    

    i’m glad to hear the best use case

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search