I just deployed an updated page in my React + Typescript SPA. This page has a carousel showing 5 out of a number of videos and autoscrolls the array of videos every 5 seconds unless the user is playing one. The page works well, or at least seems to do so. But since I deployed the page to production, the media server containing media files has crashed.
The production page gets a lot of real traffic, I guess that's why in test environment we didn't notice the problem.
My guess is that the carousel doesn't correctly unmount the video components so network requests stay "open" even the video is not showing in the carousel at that moment. But I'm not a "network guy".
This is my LandingPage component:
import { FC, useEffect, useState } from 'react';
//types
import { Testimonial } from './LandingPage.d';
// components
import SocietyInfoMobile from 'components/SocietyInfoMobile/SocietyInfoMobile';
import CtaBlock from './CtaBlock/CtaBlock.component';
import TestimonialCard from './TestimonialCard/TestimonialCard.component';
import TestimonialsCarousel from './TestimonialsCarousel';
import TestimonialsCarouselMobile from './TestimonialsCarousel/TestimonialsCarouselMobile';
// hooks
import useAppSelector from 'hooks/useAppSelector';
import { useGoogleReCaptcha } from "react-google-recaptcha-v3";
//utils
import { getTestimonials } from './services/LandingPage.services';
//assets
import { ReactComponent as BulletIcon } from './assets/bulletIcon.svg';
//components
import LoadingMask from 'components/ui/LoadingMask';
// style
import { Page, TopCardsRow, ProfitsCard, BulletsCard, BulletsItem, BulletsBox, AllTestimonialsBox, AllTestimonialsTitle, AllTestimonials } from './style/LandingPage.style';
const LandingPage: FC<{}> = () => {
const [testimonials, setTestimonials] = useState<Testimonial[]>([]);
const [isLoading, setIsLoading] = useState(true);
const { isAppMobile } = useAppSelector( state => state.ui);
const { executeRecaptcha } = useGoogleReCaptcha();
const filterSortTestimonials = (testimonials: Testimonial[]) => {
return [...testimonials
.filter(t => t.visible && t.video)
]
}
const getAllTestimonials = async () => {
try {
if (!executeRecaptcha || typeof window === 'undefined') return;
setTimeout(async () => {
const reCaptchaResponse = await executeRecaptcha('testimonials');
const response: Testimonial[] = await getTestimonials(reCaptchaResponse);
console.log('response', response);
setTestimonials(filterSortTestimonials(response));
setIsLoading(false);
}, 100); // Add a delay of 100ms or adjust accordingly
} catch (error) {
console.log(error);
}
}
useEffect(() => {
if (executeRecaptcha) {
getAllTestimonials();
}
}, [executeRecaptcha]);
return (
<Page>
<TopCardsRow>
{/* Other stuff */}
{isLoading && (
<LoadingMask
isCenter
size='big'
/>
)}
{isAppMobile && !isLoading && (
<TestimonialsCarouselMobile testimonials={testimonials}/>
)}
{!isAppMobile && !isLoading && (
<TestimonialsCarousel testimonials={testimonials}/>
)}
{/* Other Stuff */}
</Page>
);
}
export default LandingPage;
which renders the TestimonialsCarousel component:
import { FC, useEffect, useRef, useState } from "react";
//types
import { TestimonialsCarouselProps} from './TestimonialsCarousel.component.d'
//components
import TestimonialCard from '../TestimonialCard/TestimonialCard.component'
// assets
import { ReactComponent as ArrowLeftSVG } from "./assets/arrow-left-icon.svg";
import { ReactComponent as ArrowRightSVG } from "./assets/arrow-right-icon.svg";
//style
import { CardBack, CardFront, CardWrapper, Carousel, Dot, Dots, NextButton, PrevButton, SliderWrapper, TestimonialsCarouselBox } from "./style/TestimonialsCarousel.component.style";
import { getCenteredSubArray } from "./TestimonialsCarousel.helper";
const TestimonialsCarousel: FC<TestimonialsCarouselProps> = ({ testimonials }) => {
const [currentSlide, setCurrentSlide] = useState(0);
const [isVideoPaused, setIsVideoPaused] = useState(true);
const intervalRef = useRef<NodeJS.Timeout>();
const startInterval = () => {
intervalRef.current = setInterval(() => {
handleNextClick();
}, 5000);
}
const stopInterval = () => {
clearInterval(intervalRef.current);
}
const handlePrevClick = () => {
setCurrentSlide(currentSlide => currentSlide === 0 ? testimonials.length - 1 : currentSlide - 1);
stopInterval();
startInterval();
setIsVideoPaused(true)
};
const handleNextClick = () => {
setCurrentSlide(currentSlide => currentSlide === testimonials.length - 1 ? 0 : currentSlide + 1);
stopInterval();
startInterval();
setIsVideoPaused(true)
};
const handleClickCard = (title: string, cIndex: number) => {
cIndex > 0 ? handleNextClick() : handlePrevClick();
}
useEffect(() => {
if (isVideoPaused) startInterval();
if (!isVideoPaused) stopInterval();
}, [isVideoPaused]);
// const handleMouseEnter = () => {
// setIsHovering(true);
// }
// const handleMouseLeave = () => {
// setIsHovering(false);
// }
return (
<TestimonialsCarouselBox >
<Carousel>
<PrevButton onClick={handlePrevClick}>
<ArrowLeftSVG/>
</PrevButton>
<CardWrapper>
{getCenteredSubArray(testimonials, currentSlide, 5).map((testimonial, index) => {
return (
<TestimonialCard
key={testimonial.description}
testimonial={testimonial}
cIndex={index-2}
inCarousel={true}
handleClickCard={handleClickCard}
setIsVideoPaused={setIsVideoPaused}
/>
);
})}
</CardWrapper>
<NextButton onClick={handleNextClick}>
<ArrowRightSVG/>
</NextButton>
</Carousel>
<Dots>
{getCenteredSubArray(testimonials, currentSlide, 7).map((testimonial, index) => {
return (
<Dot key={testimonial.description} cIndex={index-3} />
);
})}
</Dots>
</TestimonialsCarouselBox>
);
};
export default TestimonialsCarousel;
which, finally, maps 5 testimonials array to TestimonialCard components:
import { FC, useEffect, useState } from 'react';
//hooks
import useAppSelector from 'hooks/useAppSelector';
import {useSwipeable} from 'react-swipeable'
// types
import { TestimonialCardProps } from './TestimonialCard.d';
//components
import VideoPlayerLight from 'components/VideoPlayerLight';
// style
import {
TestimonialWrapper,
VideoCard,
InfoCard,
TestimonialTitle,
TestimonialTag,
TestimonialInfo,
TestimonialLabel,
DarkOverlay,
TestimonialProfilePic,
TestimonialTagBox,
TestimonialProfilePicBackground
} from "./style/TestimonialCard.style";
const TestimonialCard: FC<TestimonialCardProps> = ({
testimonial,
cIndex,
inCarousel,
handleClickCard,
onSwipe,
setIsVideoPaused
}) => {
const [opacity, setOpacity] = useState(onSwipe !== undefined ? 0 : 1);
// Handle null/undefined properties using optional chaining and default values
const videoUrl = testimonial?.video?.url ?? '';
const coverUrl = testimonial?.videoCover?.url ?? '';
const profilePicUrl = testimonial?.profilePicture?.url ?? '';
const title = testimonial?.title ?? '';
const description = testimonial?.description ?? '';
const { isAppMobile } = useAppSelector( state => state.ui);
const onClickHandler = () => {
if (handleClickCard && title && cIndex) handleClickCard(title, cIndex)
}
const swipeHandlers = useSwipeable({
onSwipedLeft: () => {
if (onSwipe)onSwipe('left');
},
onSwipedRight: () => {
if (onSwipe) onSwipe('right');
}});
useEffect(() => {
setTimeout(() => {
setOpacity(1);
}, 50);
return () => setOpacity(0);
}, []);
return (
<TestimonialWrapper
key={description}
opacity={opacity}
inCarousel={inCarousel}
cIndex={cIndex}
onClick={onClickHandler}
{...swipeHandlers} // add swipe handlers
>
{cIndex !== undefined && !isAppMobile && <DarkOverlay cIndex={cIndex} /> }
<VideoCard inCarousel={inCarousel}>
<VideoPlayerLight videoUrl={videoUrl} posterUrl={coverUrl} setIsVideoPaused={setIsVideoPaused}/>
</VideoCard>
{/* Other stuff */}
</TestimonialWrapper>
);
};
export default TestimonialCard;
in each TestimonialCard, among other stuff, there is a VideoPlayerLight with a videoUrl and a posterUrl:
import { FC, useState, useRef } from 'react';
//types
import {VideoPlayerLightProps} from './VideoPlayerLight.component.d'
import { PlayButton, VideoElement, VideoPlayerContainer } from './style/VideoPlayerLight.component.style';
//assets
import { ReactComponent as PlayOverlayButtonSVG } from './assets/play-overlay-button.svg';
const VideoPlayerLight: FC<VideoPlayerLightProps> = ({ videoUrl, posterUrl, setIsVideoPaused }) => {
const videoRef = useRef<HTMLVideoElement>(null);
const [playing, setPlaying] = useState(false);
const [showControls, setShowControls] = useState(false);
const handlePlayButtonClick = () => {
if (videoRef.current) {
videoRef.current.play();
setPlaying(true);
setShowControls(true);
if (setIsVideoPaused) setIsVideoPaused(false);
}
};
const handleVideoClick = () => {
if (videoRef.current && !playing) {
videoRef.current.play();
setPlaying(true);
setShowControls(true);
if (setIsVideoPaused) setIsVideoPaused(false);
}
};
return (
<VideoPlayerContainer>
<VideoElement
ref={videoRef}
src={videoUrl}
poster={posterUrl}
onClick={handleVideoClick}
controls={showControls}
/>
{!playing && (
<PlayButton onClick={handlePlayButtonClick}>
<PlayOverlayButtonSVG/>
</PlayButton>
)}
</VideoPlayerContainer>
);
};
export default VideoPlayerLight;
Do you see any potential netowrk or memory leak problem with this set up?
Thanks in advance to any of you.