Home > Net >  React hook for page scroll position causing re-renders on scroll
React hook for page scroll position causing re-renders on scroll

Time:06-19

I'm using a React hook to track the scroll position on a page. The hook code is as follows:

import { useLayoutEffect, useState } from 'react';

const useScrollPosition = () => {
  const [scrollPosition, setScrollPosition] = useState(window.pageYOffset);

  useLayoutEffect(() => {
    const updatePosition = () => {
      setScrollPosition(window.pageYOffset);
    };

    window.addEventListener('scroll', updatePosition);

    return () => window.removeEventListener('scroll', updatePosition);
  }, []);

  return scrollPosition;
};

export default useScrollPosition;

I then use this in various ways, for example in this component where a class is applied to an element if the page has scrolled more than 10px:

const Component = () => {
  const scrollPosition = useScrollPosition();
  const [scrolled, setScrolled] = useState(false);

  useEffect(() => {
    const newScrolled = scrollPosition > 10;
    if (newScrolled !== scrolled) {
      setScrolled(newScrolled);
    }
  }, [scrollPosition]);

  return (
    <div
      className={clsx(style.element, {
        [style.elementScrolled]: scrolled,
      })}
    >
      {children}
    </div>
  );
};

This all works and does what I'm trying to achieve, but the component re-renders continuously on every scroll of the page.

My understanding was that by using a hook to track the scroll position, and by using useState/useEffect to only update my variable "scrolled" in the event that the scroll position passes that 10px threshold, the component shouldn't be re-rendering continuously on scroll.

Is my assumption wrong? Is this behaviour expected? Or can I improve this somehow to prevent unnecessary re-rendering? Thanks

CodePudding user response:

another idea is to have your hook react only if the scroll position is over 10pixel :


import { useEffect, useState, useRef } from 'react';

const useScrollPosition = () => {
  const [ is10, setIs10] = useState(false)

  useEffect(() => {
    const updatePosition = () => {
      if (window.pageYOffset > 10)  {setIs10(true)}
    };

    window.addEventListener('scroll', updatePosition);

    return () => window.removeEventListener('scroll', updatePosition);
  }, []);
  
  return is10;
};

export default useScrollPosition;
import React, { useEffect, useState } from "react";
import useScrollPosition from "./useScrollPosition";

const Test = ({children}) => {
  const is10 = useScrollPosition();

  useEffect(() => {
    if (is10) {
      console.log('10')
    }
  }, [is10]);

  return (
    <div
      className=''
    >
      {children}
    </div>
  );
};

export default Test

so your component Test only renders when you reach that 10px threshold, you could even pass that threshold value as a parameter to your hook, just an idea...

CodePudding user response:

Everytime there is useState, there will be a re-render. In your case you could try useRef to store the value instead of useState, as useRef will not trigger a new render

CodePudding user response:

another idea if you want to stick to your early version is a compromise :have the children of your component memoized, say you pass a children named NestedTest :

import React from 'react'

const NestedTest = () => {

  console.log('hit nested')
  return (
    <div>nested</div>
  )
}

export default React.memo(NestedTest)

you will see that the 'hit nested' does not show in the console. But that might not be what you are expecting in the first place. May be you should try utilizing useRef in your hook instead

  • Related