Search code examples
javascriptcssreactjsreact-hooksstyled-components

Matching setState interval with css animation


I am rotating the items in an array of images inside an interval of 5 seconds. Then I have a css animation with styled components that eases in the gallery images with a fade that occurs at the same time interval.

const fadeIn = keyframes`
  5%, 95% { opacity: 1 }
  100% { opacity: 0 }
`
export const Gallery = styled.div<{ lapse: number }>`
  position: relative;
  margin: 0 auto;
  max-width: 1064px;
  opacity: 0;
  animation: ${fadeIn} ease-in-out ${({ lapse }) => `${lapse}s`} infinite;
`

The problem is that when I change the state even thou at first it seems in sync, eventually the setState takes a bit longer

import React, { useState, useEffect } from 'react'
const [images, setImages] = useState<string[]>([])

// Time in seconds for each image swap, fading in between
const lapse = 5
...

useEffect(() => {
  // Clone the images array
  const imgs = [...images]
        
  // Time interval same as css animation to fade in
  const interval = setInterval(() => {
    // Take the first element and put it at the end
    imgs.push(...imgs.splice(0, 1))
    // Update the state, this seems to desync as time passes
    setImages(imgs)
  }, lapse * 1000)

  return () => clearInterval(interval)
}, [images])
        

return (
  <Gallery lapse={lapse}>
    <Portal>
      <img src={imgs/${images[0]}`}
    </Portal>
    <Thumbnails>
      <Thumbwrapper> 
        <img src={imgs/${images[1]}`}
      </Thumbwrapper>
      <Thumbwrapper> 
        <img src={imgs/${images[2]}`}
      </Thumbwrapper>
     </Thumbnails>
  </Gallery>
)

Is there a way I can make sure the swapping happends smoothly?

enter image description here


Solution

  • I came to the conclusion that having two 'clocks' to keep the time, one being react setState and the other css animation, was at the core of the problem.

    So I am now keeping a single interval with a setTimeout to make sure it goes in order then substract the time taken on the timeouts from the interval and toggle the class for css

    export const Gallery = styled.div<{ fadeIn: boolean }>`
      position: relative;
      margin: 0 auto;
      max-width: 1064px;
      transition: opacity 0.3s;
      opacity: ${({ fadeIn }) => (fadeIn ? 1 : 0)};
    `
    
    import React, { useState, useEffect } from 'react'
    const [images, setImages] = useState<string[]>([])
    const [fadeIn, setFadeIn] = useState<boolean>(true)
    
    useEffect(() => {
      let interval: ReturnType<typeof setInterval>
      if (images.length) {
    
        interval = setInterval(() => {
          setFadeIn(false)
    
          setTimeout(() => {
            setImages(images => images.slice(1).concat(images[0]) )
            setFadeIn(true)
          }, 400)
        }, (lapse - 0.4) * 1000)
      }
      return () => clearInterval(interval)
    }, [images])
    
    const getImage = (i: number) =>  <img src={imgs/${images[i]}`} />
    
    <Gallery fadeIn={fadeIn}>
      <Portal>{getImage(0)}</Portal>
      <Thumbnails>
        <Thumbwrapper>{getImage(1)}</Thumbwrapper>
        <Thumbwrapper>{getImage(2)}</Thumbwrapper>
      </Thumbnails>
    </Gallery>
    

    I did however used @Nice Books better setState aproach