React component flickers when changing state at animation end

967 Views Asked by At

I am trying to make a react slide show component with the following method:

  • The component gets an array of slides (react components) as a prop, it shows each slide at a time starting from slide number 1 (index 0).
  • The currently displayed slide (aka "current slide") is shown while the other slides are not displayed.
  • When navigating (starting the slide animation), the destination slide will be added to the wrapper. if it is before the current slide (if the destination slide has a smaller index in the slides array, aka click on next), it will be added with animation of left from -100% to 0 (slide-in from left),
    and if it's after (bigger destination index, aka clicked on previous), it will have animation from 100% to 0 (slide-in from right);
    while for the current slide, an animation class of left 0 to 100%, or left 0 to -100% will be added correspondingly (slide-out to right or left).
  • When the animation ends, the current slide will be changed to the slided-in slide (the destination slide). and the previous current slide will get removed.

Slides component (glitching):

import React, { useCallback, useState } from 'react';

import styles from './Slides.module.css';

function Slides({ slides, onBrowse, onBrowseEnd, className, ...rest }) {
  const [currentSlide, setCurrentSlide] = useState(1); //slide number

  const [animSlide, setAnimSlide] = useState(null); //destination slide number

  const browse = useCallback((slide) => {
    if (slide <= slides.length) {
      setAnimSlide(slide);
      console.log(slide);
    }
    onBrowse && onBrowse(slide);
  });

  console.log('rerender');

  const animationEndCallback = useCallback(() => {
    setCurrentSlide(animSlide);
    setAnimSlide(null);
  }, [animSlide]);

  return (
    <div className={styles.wrapper + ' ' + className} {...rest}>
      <div className={styles.slides}>

        {animSlide &&
          React.cloneElement(slides[animSlide - 1], {
            className: `
              ${styles.absolute} 
              ${styles.slide} 
              ${slides[animSlide - 1]?.props?.className} 
              ${
                animSlide < currentSlide
                  ? styles.slide_in_from_left
                  : styles.slide_in_from_right
              }
            `,
          })}

        {React.cloneElement(slides[currentSlide - 1], {
          className: `
            ${styles.absolute} 
            ${styles.slide} 
            ${slides[currentSlide - 1]?.props?.className} 
            ${
              animSlide &&
              (animSlide < currentSlide
                ? styles.slide_right
                : styles.slide_left)
            }
          `,
          onAnimationEnd: animationEndCallback,
        })}
      </div>
      {
        
      }

      <div className={styles.navcon}>
        <button
          onClick={() => {
            browse(currentSlide - 1);
          }}
          disabled={!(currentSlide > 1)}
        >
          Previuos
        </button>

        <button
          onClick={() => {
            browse(currentSlide + 1);
          }}
          disabled={
            !(animSlide
              ? animSlide < slides.length
              : currentSlide < slides.length)
          }
        >
          Next
        </button>
      </div>
    </div>
  );
}

export default Slides;

I noticed that if I put the destination slide after the current slide in the wrapper component it won't have any problem, but if I put it before, it will flicker.

I wanted to understand, why?

Not flickering (animation slide is after current slide): demo: https://stackblitz.com/edit/vitejs-vite-wnedwd?file=src%2FSlides.jsx enter image description here

flickering (animation slide is before current slide): demo: https://stackblitz.com/edit/vitejs-vite-hy6rsp?file=src%2FSlides.jsx

enter image description here

2

There are 2 best solutions below

0
Ahmed Sbai On

Difference

When the animation is running (phase A), both elements are displayed in the DOM, but when it is not(phase N), only one is displayed.
If we focus on "phase A", especially the second returned element in the JSX.
In the first example it is the one that will be displayed alone in "phase N", however, in the second example, it is not, it is the other one instead.

Impact

If we wrap the code inside your callback function with a setTimeout so we can see what's displayed on the screen after the animation ends and when both elements are present in DOM (phase A)

const animationEndCallback = useCallback(() => {
 setTimeout(()=> {
   setCurrentSlide(animSlide);
   setAnimSlide(null);
 },3000)
}, [animSlide]);

We notice that the second element returned is always the one that is shown on the screen after the animation ends.

Answer

This explains what's happening.
There is a moment when the animation is over but the component didn't rerender yet and both are present in DOM, it is in fact, that moment when the code inside the callback function is running, therefore, at that time, we see the second returned element displayed on the screen before the component rerenders and only currentSlide-1 cloned element is present. I am pretty sure this is the reason behind the flickering and why it isnot happening in the first example.

Why?

Now since I am a CSS beginner, I tried to simplify the code by getting rid of the animation and just including both elements to understand why only the second one is shown when both are present

const [currentSlide, setCurrentSlide] = useState(1); // red one
const [animSlide, setAnimSlide] = useState(0); // pink one

return (
  //...
    <div className={styles.slides}>
      {React.cloneElement(slides[animSlide], { 
        className: `
        ${slides[animSlide]?.props?.className}
        ${styles.absolute}
       `,
      })}
      {React.cloneElement(slides[currentSlide], { 
        className: `
        ${slides[currentSlide]?.props?.className}
        ${styles.absolute}
       `,
      })}
    </div>
  //...
)

If you try the above code you will see that till now only the second one is shown so this has nothing to do with the animation, however, when your try to add/remove ${styles.absolute} for both you notice that the one that takes className={styles.absolute} ie position: absolute is the one displayed and when both takes it, the last that receive it in run time is the one that is in the front and this also explains why z-index in Hadi KAR answer fix that.

absolute

The element is removed from the normal document flow, and no space is created for the element in the page layout. The element is positioned relative to its closest positioned ancestor (if any) or to the initial containing block. Its final position is determined by the values of top, right, bottom, and left.
This value creates a new stacking context when the value of z-index is not auto. The margins of absolutely positioned boxes do not collapse with other margins.


When I get rid of ${styles.absolute} in both elements, now when no one takes position: absolute I expected to see them both but the result is confusing, this time we see only the first one, makes me look into the CSS class of their wrapper div i.e styles.slides, there, you have specified flex: 1 along with overflow: hidden.
if we simplify it to this:

.slides {
  flex: 1;
}

This makes children displayed under each other following the input order, each filling the screen however if we add overflow: hidden to the class, the second one is hidden since it is overflowing.


Finally, I want to thank you for posting this question, it makes me learn new CSS stuff

2
Hadi KAR On

I tried your snippet and i can hide the flickering by making the leaving slide z-index : -1, so whatever position is animSlide or currentSlide is positioned in your tree, you can be sure the currentSlide will stay behind.

Slides.module.css

.back {
  z-index: -1;
}

Slides.js

//...
{React.cloneElement(slides[currentSlide - 1], {
          className: `
            ${styles.absolute} 
            ${styles.slide} 
            ${slides[currentSlide - 1]?.props?.className} 
            ${
              animSlide &&
              (animSlide < currentSlide
                ? styles.slide_right
                : styles.slide_left)
            }
            ${
              styles.back // add this line
            }
          `,
          onAnimationEnd: animationEndCallback,
        })}
//...