How do I make useEffect run every time a variable stored with useRef is changed?

170 views Asked by At

I am trying to have useEffect run every time it changes a variable, but changes to the variable are lost on every re-render.

If I try to store the variable between renders using useRef, useEffect doesn't run because it cannot detect changes to a ref. I found an article that shows how to effectively pass a ref to useEffect but I am not sure how to use this in the context of my problem.

here is my code where a ref is passed to useEffect:

import { useRef, useEffect, useLayoutEffect } from "react";
import "./styles.css";

export default function App() {
  const divOffset = useRef(0);
  let scrollPosition = 0;

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

  useLayoutEffect(() => {
    divOffset.current = linearInterpolation(
      divOffset.current,
      scrollPosition,
      0.07
    );
    divOffset.current = Math.floor(divOffset.current * 100) / 100;
    document.querySelector(
      "#main"
    ).style.transform = `translateX(-${divOffset.current}px);`;
  }, [scrollPosition]);

  const handleScroll = (event) => {
    scrollPosition = window.scrollY;
  };

  function linearInterpolation(x1, x2, easingValue) {
    return (1 - easingValue) * x1 + easingValue * x2;
  }

  return (
    <div id="main">
      <div className="layer1" />
      <div className="layer2" />
      <div className="layer3" />
    </div>
  );
}
1

There are 1 answers

3
Aliif On

In React, useEffect runs when the dependencies specified in its dependency array change. However, as you've noticed, changes to variables stored with useRef are not considered dependencies by default, so useEffect won't run when they change. To make useEffect run every time a variable stored with useRef changes, you can use a combination of useEffect and useRef to capture the previous value and compare it to the current value. Here's how you can modify your code to achieve this:

import { useRef, useEffect, useLayoutEffect } from "react";
import "./styles.css";

export default function App() {
  const divOffset = useRef(0);
  const scrollPositionRef = useRef(0);

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

  useLayoutEffect(() => {
    divOffset.current = linearInterpolation(
      divOffset.current,
      scrollPositionRef.current,
      0.07
    );
    divOffset.current = Math.floor(divOffset.current * 100) / 100;
    document.querySelector(
      "#main"
    ).style.transform = `translateX(-${divOffset.current}px);`;
  }, [divOffset]);

  const handleScroll = () => {
    scrollPositionRef.current = window.scrollY;
  };

  function linearInterpolation(x1, x2, easingValue) {
    return (1 - easingValue) * x1 + easingValue * x2;
  }

  return (
    <div id="main">
      <div className="layer1" />
      <div className="layer2" />
      <div className="layer3" />
    </div>
  );
}

In this modified code:

  1. Add a scrollPositionRef using useRef to store the scrollPosition. This allows us to track changes to scrollPosition across re-renders.

  2. In the useLayoutEffect, change the dependency array to [divOffset]. This means that the effect will run whenever divOffset changes. Since we're updating divOffset.current within the effect based on scrollPositionRef.current, it will run whenever scrollPositionRef.current changes.

  3. In the handleScroll function, update scrollPositionRef.current with the new scroll position.

This way, the useLayoutEffect will run every time scrollPositionRef.current changes, effectively achieving your goal of running the effect whenever the scrollPosition changes.