React 使用 useState 时滚动捕捉闪烁问题

huangapple go评论49阅读模式
英文:

React scroll-snap flicker when using useState

问题

我在使用以下组合时遇到了非常奇怪的闪烁(故障):

  • css scroll-snap
  • useState
  • 子组件

但仅在这三者的组合中!

这里是最小化的可复现代码:

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}>测试子组件</div>
  
  return (
    <div className={styles.carouselInnerContainer} ref={carouselRef}>
      <div className={styles.carouselSlide}>测试1</div>
      <div className={styles.carouselSlide}>测试2</div>
      <div className={styles.carouselSlide}>测试3</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;
}

如果我执行以下操作之一,闪烁将不会出现:

  • setCurrentScrollPos(e.target.scrollLeft) 注释掉
  • <Slide /> 注释掉
  • 将 CSS 中的 scroll-snap-align: center; 注释掉

对于这种奇怪的行为有任何想法吗?

英文:

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!

React 使用 useState 时滚动捕捉闪烁问题

Here is the minimal reproducable code:

carousel.js

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

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

  useEffect(() =&gt; {
    const carouselScrollUpdate = (e) =&gt; {
      setCurrentScrollPos(e.target.scrollLeft)
    }
    carouselRef?.current?.addEventListener(&#39;scroll&#39;, carouselScrollUpdate, { passive: true })
    return () =&gt; {
      carouselRef?.current?.removeEventListener(&#39;scroll&#39;, carouselScrollUpdate, { passive: true })
    }
  }, [carouselRef])

  const Slide = () =&gt; &lt;div className={styles.carouselSlide}&gt;Test Sub&lt;/div&gt;
  
  return (
    &lt;div className={styles.carouselInnerContainer} ref={carouselRef}&gt;
      &lt;div className={styles.carouselSlide}&gt;Test1&lt;/div&gt;
      &lt;div className={styles.carouselSlide}&gt;Test2&lt;/div&gt;
      &lt;div className={styles.carouselSlide}&gt;Test3&lt;/div&gt;
      &lt;Slide /&gt;
    &lt;/div&gt;
  )
}

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: &lt;Slide /&gt;
  • comment out: scroll-snap-align: center; in the CSS

Any ideas on that weird behaviour?

答案1

得分: 1

问题发生在您尝试在滚动位置更改时更新状态时。

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

每次调用 setCurrentScrollPos 都会导致组件中的渲染,从而引起闪烁。

相反,您可以通过使用 setTimeout 在滚动停止时观察何时设置状态。

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

或者只在满足某些条件时设置状态:

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

const isNearNextSlide = () => {
   // 添加满足条件的逻辑
}

编辑:

经过一些测试,我发现问题出现在 <Carrosel> 内部的 <Slide> 组件中,我通过将 <Slide /> 组件移到主要 <Carousel> 组件之外来解决了这个问题,防止在渲染 <Carrosel> 时重新创建 <Slide> 组件。

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>
  );
}
英文:

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

<!-- language: lang-js -->

const carouselScrollUpdate = (e) =&gt; {
  setCurrentScrollPos(e.target.scrollLeft)
}

<!-- end snippet -->

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:

<!-- language: lang-js -->

const carouselScrollUpdate = (e) =&gt; {      
    clearInterval(timer);
    timer = setTimeout(() =&gt; {
        console.log(&#39;set scroll&#39;)
        setCurrentScrollPos(e.target.scrollLeft)
    }, 500);      
}

<!-- end snippet -->

or just set your state when it satisfy some condition:

<!-- language: lang-js -->

const carouselScrollUpdate = (e) =&gt; {      
    if (isNearNextSlide()) {
      setCurrentScrollPos(e.target.scrollLeft)
    }         
}

const isNearNextSlide = () =&gt; {
   // add logic to satisfy your conditions
}

<!-- end snippet -->

Edit:

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

<!-- language: lang-js -->

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

const Slide = () =&gt; &lt;div className={styles.carouselSlide}&gt;Test Sub&lt;/div&gt;

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

  useEffect(() =&gt; {
    const carouselScrollUpdate = (e) =&gt; {
      setCurrentScrollPos(e.target.scrollLeft)
    }
    carouselRef?.current?.addEventListener(&#39;scroll&#39;, carouselScrollUpdate, { passive: true })
    return () =&gt; {
      carouselRef?.current?.removeEventListener(&#39;scroll&#39;, carouselScrollUpdate, { passive: true })
    }
  }, [carouselRef])
      
  return (
    &lt;div className={styles.carouselInnerContainer} ref={carouselRef}&gt;
      &lt;div className={styles.carouselSlide}&gt;Test1&lt;/div&gt;
      &lt;div className={styles.carouselSlide}&gt;Test2&lt;/div&gt;
      &lt;div className={styles.carouselSlide}&gt;Test3&lt;/div&gt;
      &lt;Slide /&gt;
    &lt;/div&gt;
  )
}

<!-- end snippet -->

huangapple
  • 本文由 发表于 2023年5月28日 14:16:00
  • 转载请务必保留本文链接:https://go.coder-hub.com/76350189.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定