英文:
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!
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?
答案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) => {
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) => {
clearInterval(timer);
timer = setTimeout(() => {
console.log('set scroll')
setCurrentScrollPos(e.target.scrollLeft)
}, 500);
}
<!-- end snippet -->
or just set your state when it satisfy some condition:
<!-- language: lang-js -->
const carouselScrollUpdate = (e) => {
if (isNearNextSlide()) {
setCurrentScrollPos(e.target.scrollLeft)
}
}
const isNearNextSlide = () => {
// 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 './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>
)
}
<!-- end snippet -->
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论