Search code examples
javascriptreactjsreact-hooksuse-effectuse-state

Reactjs unexpected useState behaviour in slider


I implemented autoplay Slider (having three cards) using useEffect, but manual "previous" and "forward" button are not working properly. useState is not upadating values as desired. useState is changing values that i am unable to understand that random behaviour. Once i clicked on previous or forward arrow to swipe slide than it started changing slides very quickly. Auto slider is working perfectly but i swipe manually than usestate changes value unexpectedly. how can i implement both auto and manual (forward and backward arrow) so that usestate changes value smoothly.

import React, { useState, useRef, useEffect } from "react";
import { Card, Grid } from "@material-ui/core";
import { makeStyles } from "@material-ui/core/styles";
import { SlideData, SliderImages } from "./SlideData";
import left from "../../../styles/img/homepg/AdmSlider/left.png";
import right from "../../../styles/img/homepg/AdmSlider/right.png";


function NewsSlider() {
  const classes = useStyles();

  const firstRef = useRef(null);
  const secondRef = useRef(null);
  const thirdRef = useRef(null);
  const currentRef = useRef(null);


  const Left = () => {
    setFirst(first <= 0 ? len : first - 1);
    setSecond(second <= 0 ? len : second - 1);
    setThird(third <= 0 ? len : third - 1);
  };

  const Right = () => {
    setFirst(first >= len ? 0 : first + 1);
    setSecond(second >= len ? 0 : second + 1);
    setThird(third >= len ? 0 : third + 1);
  };

  const [first, setFirst] = useState(0);
  const [second, setSecond] = useState(1);
  const [third, setThird] = useState(2);

  const length = SliderImages.length;
  const len = length - 1;
  let timeout;
  
  useEffect(() => {
    setTimeout(() => {
      setFirst(first >= len ? 0 : first + 1);
      setSecond(second >= len ? 0 : second + 1);
      setThird(third >= len ? 0 : third + 1);
       return () => clearTimeout(timeout);
    }, 3000);
  }, [first, second, third]);

  return (
    <>
      <div>
        <Grid container xs={12} className={classes.grid}>
          {" "}
          <div>
            <img
              src={left}
              alt="leftarrow"
              className={classes.left}
              onClick={Left}
            />
          </div>
          <Grid item xs={4} className={classes.card}>
            {SliderImages.map((val, index) => {
              return (
                <div>
                  {index === first && (
                    <Card className={classes.card1}>
                      <img
                        src={val.imgsrc}
                        alt={val.title}
                        className={classes.image}
                        ref={firstRef}
                      />
                    </Card>
                  )}
                  {index === second && (
                    <Card className={classes.card2}>
                      <img
                        src={val.imgsrc}
                        alt={val.title}
                        className={classes.image}
                        ref={secondRef}
                      />
                    </Card>
                  )}
                  {index === third && (
                    <Card className={classes.card3}>
                      <img
                        src={val.imgsrc}
                        alt={val.title}
                        className={classes.image}
                        ref={thirdRef}
                      />
                    </Card>
                  )}
                  <div>
                    <img
                      src={right}
                      alt="rightarrow"
                      className={classes.right}
                      onClick={Right}
                    />
                  </div>
                </div>
              );
            })}
          </Grid>
        </Grid>
      </div>
    </>
  );
}

export default NewsSlider;

Solution

  • Issue

    Your issue is you start another timeout when you manually update one of first, second, or third without clearing any previous timeouts.

    Also, timeout is redeclared each render cycle when you define it in the function body as let timeout;.

    Solution

    Capture the timeout ref and return the clearing function from the useEffect callback instead of in the timeout callback.

    useEffect(() => {
      const timeout = setTimeout(() => {
        setFirst(first >= len ? 0 : first + 1);
        setSecond(second >= len ? 0 : second + 1);
        setThird(third >= len ? 0 : third + 1);
      }, 3000);
    
      return () => clearTimeout(timeout);
    }, [first, second, third]);
    

    Alternative Solution

    Construct the slider animation to be on an interval and use functional state updates to handle prev/next slides. The functional state updates allow you to use an interval timer and correctly update from the previous slide state while also allowing you to asynchronously manually slide left or right without interrupting the interval updates.

    const Left = () => {
      setFirst(first => first <= 0 ? len : first - 1);
      setSecond(second => second <= 0 ? len : second - 1);
      setThird(third => third <= 0 ? len : third - 1);
    };
    
    const Right = () => {
      setFirst(first => first >= len ? 0 : first + 1);
      setSecond(second => second >= len ? 0 : second + 1);
      setThird(third => third >= len ? 0 : third + 1);
    };
    
    const [first, setFirst] = useState(0);
    const [second, setSecond] = useState(1);
    const [third, setThird] = useState(2);
    
    const length = SliderImages.length || 0;
    const len = length - 1;
    
    useEffect(() => {
      const interval = setInterval(() => {
        Right();
      }, 3000);
    
      return () => clearInterval(interval);
    }, []);