reactjsreact-hooksreact-transition-group

Why does useEffect() introduce a timing change in react-transition-group?


I am a bit new to hooks, and ran into some unexpected behavior with useEffect().

I am using react-transition-group to build a component that slides content left or right depending on whether you are going forward or backward. It's based on this related answer.

Running example in CodeBlitz

The transition rendering looks like this:

  // { isNext, count } -- see below
  return (
    <TransitionGroup
      childFactory={(child) =>
        React.cloneElement(child, {
          classNames: isNext ? 'right-to-left' : 'left-to-right',
          timeout: 1000,
        })
      }
    >
      <CSSTransition key={count} classNames="right-to-left" timeout={1000}>
        <div className="slide">
          Put your sliding content here. Change the key as the content changes.
          The value of the key is unimportant Count: {count}
        </div>
      </CSSTransition>
    </TransitionGroup>
  );

And the CSS:

:root {
  --duration: 1s;
}

.right-to-left-enter {
  transform: translateX(100%);
}
.right-to-left-enter-active {
  transform: translateX(0);
  transition: all var(--duration) ease;
}

.right-to-left-exit {
  transform: translateX(0);
}
.right-to-left-exit-active {
  transform: translateX(-100%);
  transition: all var(--duration) ease;
}

.left-to-right-enter {
  transform: translateX(-100%);
}
.left-to-right-enter-active {
  transform: translateX(0);
  transition: all var(--duration) ease;
}

.left-to-right-exit {
  transform: translateX(0);
}
.left-to-right-exit-active {
  transform: translateX(100%);
  transition: all var(--duration) ease;
}

The strange (to me) behavior that I'm seeing is that if I add useEffect() to track the change in count, the timing of the transition changes. A visible gap between the elements sliding in appears.

export const Slide = ({ count }) => {
  const countRef = useRef();
  useEffect(() => {
    countRef.current = count;
  }, [count]);
  // Using [count] introduces a gap between the elements transitioned in and out
  // If I remove count from deps, gap goes away (but logic is broken)

  const isNext = count > countRef.current;

In case this is some kind of browser or local environment quirk, here's what I see when I have [] as the useEffect dep:

without deps

And here's what I see when I add the [count] as the useEffect dep:

with deps

Note the white gap between the black elements as they slide across the screen.

What's going on here? How can I fix this so the animations are in sync?

Working example in CodeBlitz here.

Edit: Yes, I could make the white space go away in this example by just setting the container background to black, but (1) the real use case is not just a solid background and the gap can't be filled like that, and (2) I want to understand why useEffect and deps behave this way. :)


Solution

  • You should not use useEffect because it's executed asynchronously with some delay, so isNext will be evaluated with delay as well, and obviously this is the reason for the gap between slides.

    What you need is just to track the last two count values to calculate the sliding direction:

      const [[currCount, prevCount], setCounts] = useState([count, count]);
    
      if (count !== currCount) {
        setCounts([count, currCount]);
      }
    
      const isNext = currCount > prevCount;
    

    https://stackblitz.com/edit/react-hmsrvg?file=src%2FSlide.js,src%2FApp.js

    Update: Setting the local state during rendering is an entirely acceptable and recommended way to adjust the local state if it's depending on props. You can read more in the official React documentation here: https://beta.reactjs.org/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes