0

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.

AttemptedMastery
  • 738
  • 6
  • 21

1 Answers1

1

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).

Ori Drori
  • 183,571
  • 29
  • 224
  • 209
  • This gets me on the right track but I am using flexbox, so I have to work within the confines of the parent container. With your current implementation, it has all three images in a row and it's simply changing opacity in transition, leaving a white space behind. I'll have to tweak it to layer them. – AttemptedMastery Oct 19 '22 at 15:34
  • Put the 3 images inside a single container that would be in the flexbox. – Ori Drori Oct 19 '22 at 15:49
  • The other option is to use a transitions library. – Ori Drori Oct 19 '22 at 15:50
  • 1
    Nah, I am close to figuring it out. Too close to use a library, and too stubborn :D. I'll give you the accepted answer because the refactoring was great. Thanks for the help. – AttemptedMastery Oct 19 '22 at 15:59
  • Just an update, I implemented your solution with flexbox and the exact same thing is happening. I realized something... my initial solution was fine, as was yours. The three images are identical, other than a few changes from image to image. But... I believe some of the static parts in each image are slightly off in each of them, causing a "jitter". There's a specific building in the image that looks to be down a few pixels from one image to the next... not sure how I even fix that. – AttemptedMastery Oct 19 '22 at 16:19
  • Align the images and remove the leftovers or get better images. – Ori Drori Oct 19 '22 at 16:24