Kevin Nkonda
Kevin Nkonda

Reputation: 43

React Hooks, prop value changing randomly between renders

I'm trying to convert a class component, which is a Gatsby Layout component, to a functional component using react hooks to manage state. My goal here is to open a modal once we reach a certain point in page and scroll up.

The problem I'm facing is that one prop I'm passing to this Layout component (TrustedInView, see code below), is changing value between renders, as console logs show it. So I'm confused about what's happening there. I expect the value of the prop to always be the same since the value logged in index.js is not changing (and not supposed to).

This is the code in the layout component :

// Hook
const usePrevious = value => {
  const ref = useRef();

  // Store current value in ref
  useEffect(() => {
    ref.current = value;
  }, [value]); // Only re-run if value changes

  // Return previous value (happens before update in useEffect above)
  return ref.current;
};

let notOpenedYet = true;
// Layout component
const Layout = ({ intl, children, trustedInView }) => {
  const [modalIsOpen, setOpen] = useState(false);

  // Get the previous value (was passed into hook on last render)
  let prevPosition = usePrevious(window.scrollY);

  const handleNavigation = e => {
    const custWindow = e.currentTarget;
    if (prevPosition > custWindow.scrollY) {
      console.log('trustedInView when goes up:', trustedInView);
      if (trustedInView && notOpenedYet) {
        notOpenedYet = false;
        setTimeout(() => setOpen(true), 1500);
      }
    }
    prevPosition = custWindow.scrollY;
  };

  useEffect(() => {
    // Add event listener when component mounts:
    window.addEventListener('scroll', e => handleNavigation(e));
    // Remove event listener when component unmounts:
    return window.removeEventListener('scroll', e => handleNavigation(e));
  });

Here is the index.js render method with the layout component :

 render() {
    const { intl } = this.props;
    const { activeVideo, tabs, tabNumber, trustedInView } = this.state;
    const features = [
      // some objects
    ];
    console.log('trustedInView in index :', trustedInView);
    return (
      <Layout trustedInView={trustedInView}>

And this is the console logs :

I expect the value of the prop to always be the same

Can anyone set me on track?

Upvotes: 0

Views: 549

Answers (1)

Mon Villalon
Mon Villalon

Reputation: 692

A new listener is being set up in every render and is never removed. So in subsequent scrolls, you are actually seeing more than one console.log per event, some of them have captured a different trustedInView via the closure making it seem that the value is changing when it is not.

There are several problems with this code

  1. useEffect doesn't have any dependencies, so it's being called on every render
  2. useEffect removeEventListener is not really being called when the component unmounts, useEffect expected you to return a function and you are returning whatever the result of removeEventListener is.
  3. You are not removing any listener since you are creating a new function and passing it removeEventListener instead of the function passed to addEventListener another function created everytime.
  4. Every render has a new version of handleNavigation so even if we were to fix the previous errors we have to make sure that we remove the correct listener. Luckily this is what useCallback is for.

Taking those things into account will look something like this:

const handleNavigation = useCallback(() => { /* ... */ }, [prevPosition, trustedInView])

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

But even this is problematic since the prevPosition is going to change every time destroying the effect, plus is going to get stale for your purposes. So its better just to use a ref directly without your usePrevious hook

const prevValueRef = useRef(0);

const handleNavigation = useCallback(e => {
  const scrollY = e.currentTarget.scrollY;
  const prevScrollY = prevValueRef.current;

  // Save the value for the next event
  prevValueRef.current = scrollY;

  // ...

}, [trustedInView])

Upvotes: 1

Related Questions