如何使用 React setState 和 setInterval 循环图像轮播?

Posted

技术标签:

【中文标题】如何使用 React setState 和 setInterval 循环图像轮播?【英文标题】:How do I loop an image carousel with React setState and setInterval? 【发布时间】:2021-11-27 18:50:25 【问题描述】:

我正在尝试设置一个图像轮播,当您将鼠标悬停在 div 上时,该轮播会循环显示 3 张图像。我在尝试弄清楚如何在循环到达第三张图像后重置循环时遇到了麻烦。我需要重置 setInterval 以便当您将鼠标悬停在 div 上时它会重新启动并不断循环显示图像。然后当您将鼠标移出 div 时,循环需要停止并重置为初始状态 0。这是代码沙箱:

https://codesandbox.io/s/pedantic-lake-wn3s7

import React,  useState, useEffect  from "react";
import  images  from "./Data";
import "./styles.css";

export default function App() 
  let timer;
  const [count, setCount] = useState(0);

  const updateCount = () => 
    timer = setInterval(() => 
      setCount((prevCount) => prevCount + 1);
    , 1000);

    if (count === 3) clearInterval(timer);
  ;

  const origCount = () => 
    clearInterval(timer);
    setCount((count) => 0);
  ;

  return (
    <div className="App">
      <div className="title">Image Rotate</div>
      <div onMouseOver=updateCount onMouseOut=origCount>
        <img src=images[count].source alt=images.name />
        <p>count is: count</p>
      </div>
    </div>
  );

【问题讨论】:

【参考方案1】:

您的setCount 应该使用条件来检查它是否应该回到开始:

setCount((prevCount) => prevCount === images.length - 1 ? 0 : prevCount + 1);

如果我们在最后一张图片上,这将执行setCount(0) - 否则,它将执行setCount(prevCount + 1)

一种更快(并且可能更具可读性)的方法是:

setCount((prevCount) => (prevCount + 1) % images.length);

【讨论】:

【参考方案2】:
    每个渲染周期都会重置计时器引用,将其存储在 React ref 中,以便持续存在。 初始count 状态在区间回调范围内关闭。 只有 3 张图片,因此最后一张幻灯片的索引为 2,而不是 3。您应该与数组的长度进行比较,而不是对其进行硬编码。 您可以通过将count 状态的模乘以数组长度来计算图像索引。

代码:

export default function App() 
  const timerRef = useRef();
  const [count, setCount] = useState(0);

  // clear any running intervals when unmounting
  useEffect(() => () => clearInterval(timerRef.current), []);

  const updateCount = () => 
    timerRef.current = setInterval(() => 
      setCount((count) =>  count + 1);
    , 1000);
  ;

  const origCount = () => 
    clearInterval(timerRef.current);
    setCount(0);
  ;

  return (
    <div className="App">
      <div className="title">Image Rotate</div>
      <div onMouseOver=updateCount onMouseOut=origCount>
        <img
          src=images[count % images.length].source // <-- computed index to cycle
          alt=images.name
        />
        <p>count is: count</p>
      </div>
    </div>
  );

【讨论】:

组件卸载时无法清除活动间隔。显然在顶层没有实际意义,但在一个可能来来去去的可重用组件中,卸载后间隔会继续触发。有关示例,请参阅this sandbox。 useEffect 清理间隔是更好的方法。 @thisisrandy 问题/问题并不是关于那个,但你并没有错。清理正在运行的计时器是一件极其微不足道的事情。【参考方案3】:

任何涉及计时器/间隔的东西都是useEffect 的绝佳候选者,因为我们可以在使用effects with cleanup 设置计时器的同一位置轻松注册一个清除操作。这避免了忘记清除间隔的常见陷阱,例如当组件卸载或丢失间隔句柄时。尝试以下方法:

import React,  useState, useEffect  from "react";
import  images  from "./Data";
import "./styles.css";

export default function App() 
  const [count, setCount] = useState(0);
  const [mousedOver, setMousedOver] = useState(false);

  useEffect(() => 
    // set an interval timer if we are currently moused over
    if (mousedOver) 
      const timer = setInterval(() => 
        // cycle prevCount using mod instead of checking for hard-coded length
        setCount((prevCount) => (prevCount + 1) % images.length);
      , 1000);
      // automatically clear timer the next time this effect is fired or
      // the component is unmounted
      return () => clearInterval(timer);
     else 
      // otherwise (not moused over), reset the counter
      setCount(0);
    
    // the dependency on mousedOver means that this effect is fired
    // every time mousedOver changes
  , [mousedOver]);

  return (
    <div className="App">
      <div className="title">Image Rotate</div>
      <div
        // just set mousedOver here instead of calling update/origCount
        onMouseOver=() => setMousedOver(true)
        onMouseOut=() => setMousedOver(false)
      >
        <img src=images[count].source alt=images.name />
        <p>count is: count</p>
      </div>
    </div>
  );

至于你的代码为什么不起作用,有几件事:

    你的意思是说if (count === 2) ...,而不是count === 3。更好的是使用 images 数组的长度而不是硬编码 此外,count 的值是stale inside of the closure,即在您使用setCount 更新它之后,count 的旧值仍被捕获在updateCount 内部。这实际上是使用功能状态更新的原因,当您说例如setCount((prevCount) =&gt; prevCount + 1) 您需要在区间内循环计数,而不是在鼠标悬停时清除区间。如果你仔细思考它的逻辑,这应该是显而易见的 通常在反应中,使用像timer 这样的函数局部变量不会达到您的预期。总是使用状态和效果,在极少数情况下(不是这个),其他一些钩子,比如refs

【讨论】:

【参考方案4】:

我认为setInterval 不适用于函数组件。由于回调通过闭包访问变量,因此很容易自找麻烦,要么得到引用陈旧值的计时器回调,要么甚至有多个时间间隔同时运行。不是说你无法克服这个问题,但使用setTimeout 更容易使用

useEffect(() => 
  if(state === 3) return;
  const timerId = setTimeout(() => setState(old => old + 1), 5000);
  return () => clearTimeout(timerId);
, [state]);

在这种特殊情况下可能不需要清理 (clearTimeout),但例如,如果用户能够手动切换图像,我们希望延迟下一次自动更改。

【讨论】:

以上是关于如何使用 React setState 和 setInterval 循环图像轮播?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 usestate 和 setstate 而不是 this.setState 使用 react 和 typescript?

如何使用 React setState 和 setInterval 循环图像轮播?

Set State into getDerivedStateFromProps (set state, React)

React:如何使用功能性 usestate/useEffect 复制特定的组件类 setstate 回调示例?

React的setState如何实现同步处理数据

如何在 React JS 中的 css 转换结束后使用 setState?