I have an image carousel component which has a smooth transition between images using the eventListener
transtionend
.
This event listener even though I have a cleanup function in place it creates a memory leak. When I leave the page that has the image carousel the error does not appear yet. However, if I return to the page with the carousel and the transition completes one cycle (the image changes) then I get the error in the console.
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
I attached my 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("resize", onResize);
window.removeEventListener("focus", onFocus);
window.removeEventListener("blur", onBlur);
window.removeEventListener("transitionend", transitionEnd);
};
//eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
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 window listener is getting removed at the end of the useEffect
but I don't know why it still creates the memory leak.
Hmm. It seems you're removing event listeners incorrectly. DOM addEventListener
returns nothing (undefined).
Wrong:
const onResize = window.addEventListener("resize", resize);
window.removeEventListener("resize", onResize);
Should be:
window.addEventListener("resize", resize);
window.removeEventListener("resize", resize);