Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I loop an image carousel with React setState and setInterval?

I am trying to set up an image carousel that loops through 3 images when you mouseover a div. I'm having trouble trying to figure out how to reset the loop after it reaches the third image. I need to reset the setInterval so it starts again and continuously loops through the images when you are hovering over the div. Then when you mouseout of the div, the loop needs to stop and reset to the initial state of 0. Here is the Code Sandbox:

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>
  );
}
like image 428
typeyes Avatar asked Oct 19 '25 16:10

typeyes


2 Answers

Anything involving timers/intervals is an excellent candidate for useEffect, because we can easily register a clear action in the same place that we set the timer using effects with cleanup. This avoids the common pitfalls of forgetting to clear an interval, e.g. when the component unmounts, or losing track of interval handles. Try something like the following:

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>
  );
}

Edit hungry-einstein-d8pe0

As to why your code didn't work, a few things:

  1. You meant to say if (count === 2) ..., not count === 3. Even better would be to use the length of the images array instead of hardcoding it
  2. Moreover, the value of count was stale inside of the closure, i.e. after you updated it using setCount, the old value of count was still captured inside of updateCount. This is actually the reason to use functional state updates, which you did when you said e.g. setCount((prevCount) => prevCount + 1)
  3. You would have needed to loop the count inside the interval, not clear the interval on mouse over. If you think through the logic of it carefully, this should hopefully be obvious
  4. In general in react, using a function local variable like timer is not going to do what you expect. Always use state and effects, and in rarer cases (not this one), some of the other hooks like refs
like image 90
thisisrandy Avatar answered Oct 21 '25 04:10

thisisrandy


I believe that setInterval does not work well with function components. Since callback accesses variables through closure, it's really easy to shoot own foot and either get timer callback referring to stale values or even have multiple intervals running concurrently. Not telling you cannot overcome that, but using setTimeout is much much much easier to use

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

Maybe in this particular case cleanup(clearTimeout) is not required, but for example if user is able to switch images manually, we'd like to delay next auto-change.

like image 33
skyboyer Avatar answered Oct 21 '25 05:10

skyboyer



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!