1

I am experiencing a very weird flickering (glitch) when using the combination of

  • css scroll-snap
  • useState
  • Sub-Components

But ONLY in the combination of these three!

flickering

Here is the minimal reproducable code:

carousel.js

import styles from './carousel.module.scss'
import { useEffect, useRef, useState } from 'react';

export default function Carousel() {
  const [currentScollPos, setCurrentScrollPos] = useState(0)
  const carouselRef = useRef()

  useEffect(() => {
    const carouselScrollUpdate = (e) => {
      setCurrentScrollPos(e.target.scrollLeft)
    }
    carouselRef?.current?.addEventListener('scroll', carouselScrollUpdate, { passive: true })
    return () => {
      carouselRef?.current?.removeEventListener('scroll', carouselScrollUpdate, { passive: true })
    }
  }, [carouselRef])

  const Slide = () => <div className={styles.carouselSlide}>Test Sub</div>
  
  return (
    <div className={styles.carouselInnerContainer} ref={carouselRef}>
      <div className={styles.carouselSlide}>Test1</div>
      <div className={styles.carouselSlide}>Test2</div>
      <div className={styles.carouselSlide}>Test3</div>
      <Slide />
    </div>
  )
}

carousel.module.scss

.carouselInnerContainer {
  display: flex;
  flex-wrap: nowrap;
  overflow-x: scroll;
  scroll-snap-type: x mandatory;
}

.carouselSlide {
  flex: 0 0 auto;
  width: 50%;
  margin-left: 2rem;
  background-color: aquamarine;
  height: 200px;
  scroll-snap-align: center;
}

The flickering will NOT be there if I do ONE of the following:

  • comment out: setCurrentScrollPos(e.target.scrollLeft)
  • comment out: <Slide />
  • comment out: scroll-snap-align: center; in the CSS

Any ideas on that weird behaviour?

somecoder
  • 53
  • 6
  • Where do you use currentScollPos? – Jonathas Nascimento May 28 '23 at 13:28
  • In the minimal reproducable example: nowhere. in reality: to calculate the nearest slide in view. But you could imagine something like: `useEffect(() =>{console.log(currentScollPos)},[currentScollPos])` if that helps. Unfortunately it does not make a difference whether it is used or not. – somecoder May 28 '23 at 13:52

1 Answers1

1

The problem occurs when you try update the state every time the scroll position changes

const carouselScrollUpdate = (e) => {
  setCurrentScrollPos(e.target.scrollLeft)
}

Each setCurrentScrollPos will cause a renderer in your component, causing it to flicker

Instead set state every time you can observer when the scroll stops using setTimout:

const carouselScrollUpdate = (e) => {      
    clearInterval(timer);
    timer = setTimeout(() => {
        console.log('set scroll')
        setCurrentScrollPos(e.target.scrollLeft)
    }, 500);      
}

or just set your state when it satisfy some condition:

const carouselScrollUpdate = (e) => {      
    if (isNearNextSlide()) {
      setCurrentScrollPos(e.target.scrollLeft)
    }         
}

const isNearNextSlide = () => {
   // add logic to satisfy your conditions
}

Edit:

After some testing I saw the problem is the inner Slide component inside and I managed to fix it by moving the component outside the main component, preventing that the component from being recreated when rendering

import styles from './carousel.module.scss'
import { useEffect, useRef, useState } from 'react';

const Slide = () => <div className={styles.carouselSlide}>Test Sub</div>

export default function Carousel() {
  const [currentScollPos, setCurrentScrollPos] = useState(0)
  const carouselRef = useRef()

  useEffect(() => {
    const carouselScrollUpdate = (e) => {
      setCurrentScrollPos(e.target.scrollLeft)
    }
    carouselRef?.current?.addEventListener('scroll', carouselScrollUpdate, { passive: true })
    return () => {
      carouselRef?.current?.removeEventListener('scroll', carouselScrollUpdate, { passive: true })
    }
  }, [carouselRef])
      
  return (
    <div className={styles.carouselInnerContainer} ref={carouselRef}>
      <div className={styles.carouselSlide}>Test1</div>
      <div className={styles.carouselSlide}>Test2</div>
      <div className={styles.carouselSlide}>Test3</div>
      <Slide />
    </div>
  )
}
  • Thank you for your answer! I am absolutely aware of the rerender. There is a ton of examples using useState to store the scroll-position (i.e. https://designcode.io/react-hooks-handbook-usescrollposition-hook). So it does not seem to be bad practice. AND: if I comment out the sub-component everything works as expected. So the rerender-cycle of react **cannot** be the source of the proplem! I like the setTimeout workaround anyway. So thank you! – somecoder May 28 '23 at 16:36
  • By the way: I already tried the second idea (set state conditional): this causes the same proplem! – somecoder May 28 '23 at 16:39
  • I tried your first idea (setTimeout). Really scary: it is still the exact same flickering but only in 500ms slow-motion. at the very same moment as "set scroll" is displayed in the console, the scroll-bar snaps back to the next scroll-snap-position and then flips back to the current mouse cursor. so it seems to be closely connected to the css scroll-snap-feature. – somecoder May 28 '23 at 19:43
  • @somecoder Please check my last update in the answer – Jonathas Nascimento May 28 '23 at 22:15