ReactJS: Image slider setTimeout gives memory leak

  javascript, memory-leaks, reactjs, settimeout

I have a image carousel component that it automatically moves to the next image in n seconds (autoPlay prop).

Also, the user can press the arrow buttons that are at each end of the image and go between the images itself.

In order for me to avoid the user clicking an arrow at the same time the carousel automatically goes to the next image itself I have created an useEffect hook which disables the arrow buttons for a second while the carousel changes images using setTimeout. I commented the useEffect hook in the code.

I have attached the code below :

/** @jsx jsx */
import { useState, useEffect, useRef } from "react";
import { css, jsx } from "@emotion/core";

import SliderContent from "./SliderContent";
import Slide from "./Slide";
import Arrow from "./Arrow";
import Dots from "./Dots";

export default function Slider({ autoPlay }) {
  const getWidth = () => window.innerWidth * 0.8;

  const slides = [
    "https://images.unsplash.com/photo-1449034446853-66c86144b0ad?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2100&q=80",
    "https://images.unsplash.com/photo-1470341223622-1019832be824?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2288&q=80",
    "https://images.unsplash.com/photo-1448630360428-65456885c650?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2094&q=80",
    "https://images.unsplash.com/photo-1534161308652-fdfcf10f62c4?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2174&q=80",
  ];

  const firstSlide = slides[0];
  const secondSlide = slides[1];
  const lastSlide = slides[slides.length - 1];

  const [isTabFocused, setIsTabFocused] = useState(true);
  const [isButtonDisabled, setIsButtonDisabled] = useState(false);

  const [state, setState] = useState({
    translate: 0,
    transition: 0.9,
    activeSlide: 0,
    _slides: [firstSlide, secondSlide, lastSlide],
  });

  const { activeSlide, translate, _slides, transition } = state;

  const autoPlayRef = useRef();
  const transitionRef = useRef();
  const resizeRef = useRef();
  const focusedTabRef = useRef();
  const blurredTabRef = useRef();

  useEffect(() => {
    //eslint-disable-next-line react-hooks/exhaustive-deps
    if (transition === 0) setState({ ...state, transition: 0.9 });
  }, [transition]);

  useEffect(() => {
    transitionRef.current = smoothTransition;
    resizeRef.current = handleResize;
    focusedTabRef.current = handleFocus;
    blurredTabRef.current = handleBlur;
    autoPlayRef.current = handleAutoPlay;
  });

  useEffect(() => {
    const play = () => autoPlayRef.current();

    let interval = null;

    if (autoPlay) {
      interval = setInterval(play, autoPlay * 1000);
    }

    return () => {
      if (autoPlay) {
        clearInterval(interval);
      }
    };
    //eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isButtonDisabled, autoPlay]);

  useEffect(() => {
    const smooth = (e) => {
      if (typeof e.target.className === "string" || e.target.className instanceof String) {
        if (e.target.className.includes("SliderContent")) {
          transitionRef.current();
        }
      }
    };
    const resize = () => resizeRef.current();
    const onFocusAction = () => focusedTabRef.current();
    const onBlurAction = () => blurredTabRef.current();

    const transitionEnd = window.addEventListener("transitionend", smooth);
    const onResize = window.addEventListener("resize", resize);
    const onFocus = window.addEventListener("focus", onFocusAction);
    const onBlur = window.addEventListener("blur", onBlurAction);

    return () => {
      window.removeEventListener("transitionend", transitionEnd);
      window.removeEventListener("resize", onResize);
      window.removeEventListener("focus", onFocus);
      window.removeEventListener("blur", onBlur);
    };
    //eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  useEffect(() => { // the button disabling hook.
    if (isButtonDisabled) {
      const buttonTimeout = setTimeout(() => {
        setIsButtonDisabled(false);
      }, 1000);

      return () => clearTimeout(buttonTimeout);
    }
    //eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isButtonDisabled]);

  const handleFocus = () => setIsTabFocused(true);
  const handleBlur = () => setIsTabFocused(false);
  const handleAutoPlay = () => isTabFocused && nextSlide();
  const handleResize = () => setState({ ...state, translate: getWidth(), transition: 0 });

  const nextSlide = () => {
    if (!isButtonDisabled) {
      setState({
        ...state,
        translate: translate + getWidth(),
        activeSlide: activeSlide === slides.length - 1 ? 0 : activeSlide + 1,
      });
    }

    setIsButtonDisabled(true);
  };

  const prevSlide = () => {
    if (!isButtonDisabled) {
      setState({
        ...state,
        translate: 0,
        activeSlide: activeSlide === 0 ? slides.length - 1 : activeSlide - 1,
      });
    }

    setIsButtonDisabled(true);
  };

  const smoothTransition = () => {
    let _slides = [];

    // We're at the last slide.
    if (activeSlide === slides.length - 1)
      _slides = [slides[slides.length - 2], lastSlide, firstSlide];
    // We're back at the first slide. Just reset to how it was on initial render
    else if (activeSlide === 0) _slides = [lastSlide, firstSlide, secondSlide];
    // Create an array of the previous last slide, and the next two slides that follow it.
    else _slides = slides.slice(activeSlide - 1, activeSlide + 2);

    setState({
      ...state,
      _slides,
      transition: 0,
      translate: getWidth(),
    });
  };

  return (
    <div css={SliderCSS}>
      <SliderContent
        translate={translate}
        transition={transition}
        width={getWidth() * _slides.length}
      >
        {_slides.map((slide, i) => (
          <Slide width={getWidth()} key={slide + i} content={slide} />
        ))}
      </SliderContent>

      <Arrow direction="left" handleClick={prevSlide} isDisabled={isButtonDisabled} />
      <Arrow direction="right" handleClick={nextSlide} isDisabled={isButtonDisabled} />

      <Dots slides={slides} activeIndex={activeSlide} />
    </div>
  );
}

const SliderCSS = css`
  position: relative;
  height: 600px;
  width: 80%;
  margin: 40px auto 0px auto;
  overflow: hidden;
`;

The issue I am experiencing is that when I go to another page on the website while the image is getting changed there is no memory leak in the console. But when I go back to the page with the slider when the image gets changed either by the user or automatically and therefore the setTimeout is getting called in the useEffect hook then I get the Can’t perform a React state update on an unmounted component. This indicates a memory leak error.

I am not sure why I get the memory leak when I am back on the page. I am cleaning up the setTimeout properly on the useEffect. Any ideas?

Source: Ask Javascript Questions

LEAVE A COMMENT