Home > Back-end >  Layering images based on truthy values causes a subtle jitter when mounting
Layering images based on truthy values causes a subtle jitter when mounting

Time:10-20

I have three sequential images that are being appended to the DOM using setInterval within useEffect. The end product looks like a interactive image and it works great, minus the fact that there's a very, very subtle jitter when each image is mounted. I wrote this very quickly and have not refactored, so forgive the verbosity.

import styles from './HeroImageSection.module.css'
import Image from 'next/image'
import React, { useState, useEffect, useRef } from 'react';
import Link from 'next/link'
import slideOne from  './../../../public/assets/hero_images/1_nonumber.png'
import slideTwo from  './../../../public/assets/hero_images/2_nonumber.png'
import slideThree from  './../../../public/assets/hero_images/3_nonumber.png'
import Logo65Font from '../../global/ui/Logo65Font.js'


export default function HeroImageSection() {

    const [isShowing, setIsShowing] = useState({
        slideOne: true,
        slideTwo: '',
        slideThree:  ''
    })

    useEffect(() =>{
        let timer = setInterval(() => {
            if (isShowing['slideOne'] === true){
                setIsShowing(prevState => ({...prevState, ['slideOne']: false}))
                setIsShowing(prevState => ({...prevState, ['slideTwo']: true}))
                setIsShowing(prevState => ({...prevState, ['slideThree']: false}))
            } else if (isShowing['slideThree'] === true && isShowing['slideOne'] === false){
                setIsShowing(prevState => ({...prevState, ['slideOne']: true}))
                setIsShowing(prevState => ({...prevState, ['slideTwo']: false}))
                setIsShowing(prevState => ({...prevState, ['slideThree']: false}))
            } else if (isShowing['slideOne'] === true && isShowing['slideTwo'] === false){
                setIsShowing(prevState => ({...prevState, ['slideOne']: false}))
                setIsShowing(prevState => ({...prevState, ['slideTwo']: true}))
                setIsShowing(prevState => ({...prevState, ['slideThree']: false}))
            } else if (isShowing['slideTwo'] === true && isShowing['slideThree'] === false){
                setIsShowing(prevState => ({...prevState, ['slideOne']: false}))
                setIsShowing(prevState => ({...prevState, ['slideTwo']: false}))
                setIsShowing(prevState => ({...prevState, ['slideThree']: true}))
            }
        }, 1000)
        return () => clearInterval(timer);
    }, [isShowing])



    return(
        <section id={styles.heroImageSectionContainer}>
            <div id={styles.centerImageTextContainer}>
            <div id={styles.mobileLogo}>
                <Logo65Font />
            </div>
            <div id={styles.heroTextContainer}>
                <h1>Untouched pallets ready to be delivered to your driveway<span id={styles.periodColor}>.</span></h1>
                    <p><Link href={`/register`}><span id={styles.signUpUnderline}>Sign up</span></Link> to see shipping rates as you shop</p>
            </div>
            <div id={styles.heroImageContainer}>
                <div className={isShowing['slideOne'] === true ? styles.centerImage : styles.displayNone}>
                    <Image alt="The flow of goods from retailer origin to your house." src={slideOne} priority={true} />
                </div>
                <div className={isShowing['slideTwo'] === true ? styles.centerImage : styles.displayNone}>
                    <Image alt="The flow of goods from retailer origin to your house." src={slideTwo} priority={true} />
                </div>
                <div className={isShowing['slideThree'] === true ? styles.centerImage : styles.displayNone}>
                    <Image alt="The flow of goods from retailer origin to your house." src={slideThree} priority={true}  />
                </div>
            </div>
            </div>
        </section>
        )
}

I am using boolean values per image to track whether or not they should be displayed depending on the state of the other two. Depending on truthiness, I either use a class that has display: none or I add the necessary styling class. Obviously, the jitter is because of the removal of the classes but I am not sure how I can smooth that out, if at all?

Let's say we do it the "react way" and shy away from manipulating classes like so:

            {isShowing['slideOne'] === true ?
                <div className={styles.centerImage}>
                    <Image alt="The flow of goods from retailer origin to your house." src={slideOne} priority={true} />
                </div>
            : ''}
            {isShowing['slideTwo'] === true ?
                <div className={styles.centerImage}>
                    <Image alt="The flow of goods from retailer origin to your house." src={slideTwo} priority={true} />
                </div>
            : ''}

            {isShowing['slideThree'] === true ?
                <div className={styles.centerImage}>
                    <Image alt="The flow of goods from retailer origin to your house." src={slideThree} priority={true} />
                </div>
            : ''}

The end result is the same, except the first run through of the transition causes a flicker, so it actually adds another bug to the process.

I am using a flexbox layout and here is what the centerImage class looks like:

    #centerImage{
        display: flex;
        width: 70%;
        height: 100%;
        justify-content: center;
    }

Hopefully you guys have some suggestions!

EDIT: The images are all the exact same size too.

CodePudding user response:

You use condition rendering to hide/show images. When the image is mounted for the first time it's loaded, and that causes the jitter.

Mount all images at the same time, and use a class to hide/show them, and a transition to smoothen the change.

const { useState, useEffect } = React;

const images = [
  'https://picsum.photos/id/236/400/600',
  'https://picsum.photos/id/237/400/600',
  'https://picsum.photos/id/238/400/600'
];

function HeroImageSection() {
  const [currentImage, setCurrentImage] = useState(0)

  useEffect(() =>{
    const timer = setInterval(() => {
      setCurrentImage(prev => (prev   1) % images.length);      
    }, 1000);

    return () => clearInterval(timer);
  }, [])

  return(
    <div>
      {images.map((url, idx) => 
        <img key={url} src={url} className={`image ${idx === currentImage ? 'show-image' : ''}`} />
      )}
    </div>
  )
}

ReactDOM
  .createRoot(root)
  .render(<HeroImageSection />);
.container {
  position: relative;
  height: 600px;
  width: 400px;
}

.image {
  position: absolute;
  top: 0;
  left: 0;
  opacity: 0;
  transition: opacity 0.5s;
}

.show-image {
  opacity: 1;
}
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>

<div id="root"></div>

Note: if you have large images, loading might still take more time. Try to compress the images, and also consider starting the animation only after all images loaded (see examples here).

  • Related