I'm trying to build a Movie Search web app with React. I query themoviedb.com for movie information and display the poster/movie title to the user.
I'm using Swiperjs to display the movie posters and titles to the users horizontally. Unfortunately, when I map() the movie information into a SwiperSlide component, the slider won't slide. It seems to stutter and refuse to move from the first movie poster. Can anyone send me in the right direction as to why it won't work?
Weirdly enough, on random refreshes, random Genres will work completely fine until a reload, but only one Genre component will do this.
Genre Component:
import React from "react";
import { Swiper, SwiperSlide } from 'swiper/react';
import MovieCard from '../components/movieCard';
class Genre extends React.Component {
constructor(props) {
super(props);
this.state = {
genre: this.props.genre,
genreName: "",
movies: []
};
}
componentDidMount() {
const genreNum = this.state.genre;
const url = `apiURlRequestHere`;
try {
fetch(url)
.then((response) => response.json())
.then((data) => this.setState({ movies: data.results }));
} catch (err) {
console.error(err);
}
this.getGenreName(genreNum);
}
getGenreName(genreNum) {
let title = "";
switch (genreNum) {
case "28":
title = "Action";
break;
case "12":
title = "Adventure";
break;
case "16":
title = "Animation";
break;
case "35":
title = "Comedy";
break;
case "80":
title = "Crime";
break;
case "99":
title = "Documentary";
break;
case "18":
title = "Drama";
break;
case "14":
title = "Fantasy";
break;
case "27":
title = "Horror";
break;
case "9648":
title = "Mystery";
break;
case "10749":
title = "Romance";
break;
case "878":
title = "Science Fiction";
break;
case "53":
title = "Thriller";
break;
default:
title = "";
break;
}
this.setState({ genreName: title });
}
render() {
const movies = this.state.movies;
let genreCat = this.state.genreName;
return (
<>
<h2 className="category-title">{genreCat}</h2>
<Swiper spaceBetween={0} slidesPerView={1}>
{movies
.filter((movie) => movie.poster_path)
.map((movie) => (
<SwiperSlide key={movie.id}>
<MovieCard movie={movie} />
</SwiperSlide>
))}
</Swiper>
</>
);
}
}
export default Genre;
MovieCard Component:
import React from "react";
export default function MovieCard({movie}) {
return (
<div className="card">
<img
className="card--image"
src={`https://image.tmdb.org/t/p/w185_and_h278_bestv2/${movie.poster_path}`}
alt={movie.title + ' poster'}
/>
<div className="card--content">
<h3 className="card--title">{movie.title}</h3>
<p className="card--rating">{movie.vote_average * 10}%</p>
</div>
</div>
)
}
--- UPDATE ---
Here is the Swiper Component I am using from SwiperJs
import React, { useRef, useState, useEffect, forwardRef } from 'react';
import { getParams } from './get-params';
import { initSwiper } from './init-swiper';
import { needsScrollbar, needsNavigation, needsPagination, uniqueClasses } from './utils';
import { renderLoop, calcLoopedSlides } from './loop';
import { getChangedParams } from './get-changed-params';
import { getChildren } from './get-children';
import { updateSwiper } from './update-swiper';
import { renderVirtual, updateOnVirtualData } from './virtual';
import { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect';
const Swiper = forwardRef(
(
{
className,
tag: Tag = 'div',
wrapperTag: WrapperTag = 'div',
children,
onSwiper,
...rest
} = {},
externalElRef,
) => {
const [containerClasses, setContainerClasses] = useState('swiper-container');
const [virtualData, setVirtualData] = useState(null);
const [breakpointChanged, setBreakpointChanged] = useState(false);
const initializedRef = useRef(false);
const swiperElRef = useRef(null);
const swiperRef = useRef(null);
const oldPassedParamsRef = useRef(null);
const oldSlides = useRef(null);
const nextElRef = useRef(null);
const prevElRef = useRef(null);
const paginationElRef = useRef(null);
const scrollbarElRef = useRef(null);
const { params: swiperParams, passedParams, rest: restProps } = getParams(rest);
const { slides, slots } = getChildren(children);
const changedParams = getChangedParams(
passedParams,
oldPassedParamsRef.current,
slides,
oldSlides.current,
);
oldPassedParamsRef.current = passedParams;
oldSlides.current = slides;
const onBeforeBreakpoint = () => {
setBreakpointChanged(!breakpointChanged);
};
Object.assign(swiperParams.on, {
_containerClasses(swiper, classes) {
setContainerClasses(classes);
},
_swiper(swiper) {
swiper.loopCreate = () => {};
swiper.loopDestroy = () => {};
if (swiperParams.loop) {
swiper.loopedSlides = calcLoopedSlides(slides, swiperParams);
}
swiperRef.current = swiper;
if (swiper.virtual && swiper.params.virtual.enabled) {
swiper.virtual.slides = slides;
swiper.params.virtual.cache = false;
swiper.params.virtual.renderExternal = setVirtualData;
swiper.params.virtual.renderExternalUpdate = false;
}
},
});
if (swiperRef.current) {
swiperRef.current.on('_beforeBreakpoint', onBeforeBreakpoint);
}
useEffect(() => {
return () => {
if (swiperRef.current) swiperRef.current.off('_beforeBreakpoint', onBeforeBreakpoint);
};
});
// set initialized flag
useEffect(() => {
if (!initializedRef.current && swiperRef.current) {
swiperRef.current.emitSlidesClasses();
initializedRef.current = true;
}
});
// watch for params change
useIsomorphicLayoutEffect(() => {
if (changedParams.length && swiperRef.current && !swiperRef.current.destroyed) {
updateSwiper(swiperRef.current, slides, passedParams, changedParams);
}
});
// update on virtual update
useIsomorphicLayoutEffect(() => {
updateOnVirtualData(swiperRef.current);
}, [virtualData]);
// init swiper
useIsomorphicLayoutEffect(() => {
if (externalElRef) {
externalElRef.current = swiperElRef.current;
}
if (!swiperElRef.current) return;
initSwiper(
{
el: swiperElRef.current,
nextEl: nextElRef.current,
prevEl: prevElRef.current,
paginationEl: paginationElRef.current,
scrollbarEl: scrollbarElRef.current,
},
swiperParams,
);
if (onSwiper) onSwiper(swiperRef.current);
// eslint-disable-next-line
return () => {
if (swiperRef.current && !swiperRef.current.destroyed) {
swiperRef.current.destroy();
}
};
}, []);
// bypass swiper instance to slides
function renderSlides() {
if (swiperParams.virtual) {
return renderVirtual(swiperRef.current, slides, virtualData);
}
if (!swiperParams.loop || (swiperRef.current && swiperRef.current.destroyed)) {
return slides.map((child) => {
return React.cloneElement(child, { swiper: swiperRef.current });
});
}
return renderLoop(swiperRef.current, slides, swiperParams);
}
return (
<Tag
ref={swiperElRef}
className={uniqueClasses(`${containerClasses}${className ? ` ${className}` : ''}`)}
{...restProps}
>
{slots['container-start']}
{needsNavigation(swiperParams) && (
<>
<div ref={prevElRef} className="swiper-button-prev" />
<div ref={nextElRef} className="swiper-button-next" />
</>
)}
{needsScrollbar(swiperParams) && <div ref={scrollbarElRef} className="swiper-scrollbar" />}
{needsPagination(swiperParams) && (
<div ref={paginationElRef} className="swiper-pagination" />
)}
<WrapperTag className="swiper-wrapper">
{slots['wrapper-start']}
{renderSlides()}
{slots['wrapper-end']}
</WrapperTag>
{slots['container-end']}
</Tag>
);
},
);
Swiper.displayName = 'Swiper';
export { Swiper };
And here is the SwiperSlide Component:
import React, { useRef, useState, forwardRef } from 'react';
import { uniqueClasses } from './utils';
import { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect';
const SwiperSlide = forwardRef(
(
{ tag: Tag = 'div', children, className = '', swiper, zoom, virtualIndex, ...rest } = {},
externalRef,
) => {
const slideElRef = useRef(null);
const [slideClasses, setSlideClasses] = useState('swiper-slide');
function updateClasses(swiper, el, classNames) {
if (el === slideElRef.current) {
setSlideClasses(classNames);
}
}
useIsomorphicLayoutEffect(() => {
if (externalRef) {
externalRef.current = slideElRef.current;
}
if (!slideElRef.current || !swiper) return;
if (swiper.destroyed) {
if (slideClasses !== 'swiper-slide') {
setSlideClasses('swiper-slide');
}
return;
}
swiper.on('_slideClass', updateClasses);
// eslint-disable-next-line
return () => {
if (!swiper) return;
swiper.off('_slideClass', updateClasses);
};
});
let slideData;
if (typeof children === 'function') {
slideData = {
isActive:
slideClasses.indexOf('swiper-slide-active') >= 0 ||
slideClasses.indexOf('swiper-slide-duplicate-active') >= 0,
isVisible: slideClasses.indexOf('swiper-slide-visible') >= 0,
isDuplicate: slideClasses.indexOf('swiper-slide-duplicate') >= 0,
isPrev:
slideClasses.indexOf('swiper-slide-prev') >= 0 ||
slideClasses.indexOf('swiper-slide-duplicate-prev') >= 0,
isNext:
slideClasses.indexOf('swiper-slide-next') >= 0 ||
slideClasses.indexOf('swiper-slide-duplicate next') >= 0,
};
}
const renderChildren = () => {
return typeof children === 'function' ? children(slideData) : children;
};
return (
<Tag
ref={slideElRef}
className={uniqueClasses(`${slideClasses}${className ? ` ${className}` : ''}`)}
data-swiper-slide-index={virtualIndex}
{...rest}
>
{zoom ? (
<div
className="swiper-zoom-container"
data-swiper-zoom={typeof zoom === 'number' ? zoom : undefined}
>
{renderChildren()}
</div>
) : (
renderChildren()
)}
</Tag>
);
},
);
SwiperSlide.displayName = 'SwiperSlide';
export { SwiperSlide };
Also, I have a working example with the issue here... https://codesandbox.io/s/blue-mountain-0fjyc?file=/src/App.js in this example, hit refresh on the browser and that is what I see.
--- Update ---
Still working on this project and I created a movie detail page that displays the cast with a Swiper(what a fool, I know). It works perfectly fine but if I replace the code in the GenreComponent code with the working code from the below MovieDetail component, it still will not work...
import React from "react";
import { Swiper, SwiperSlide } from 'swiper/react';
import MovieGenre from '../components/movieGenreBtn';
class MovieDetails extends React.Component {
constructor(props) {
super(props);
this.state = {
movie: [],
movieGenres: [],
credits: [],
director: [],
foundDirector: false
}
}
componentDidMount() {
const movieId = this.props.location.pathname.replace("/", "");
this.fetchMovie(movieId);
this.fetchCrew(movieId);
}
fetchMovie(movieId) {
const url = `https://api.themoviedb.org/3/movie/${movieId}?api_key=${process.env.REACT_APP_MOVIE_API_KEY}&language=en-US
`;
try {
fetch(url)
.then((response) => response.json())
.then((data) => this.setState({ movie: data }));
} catch (err) {
console.error(err);
}
}
fetchCrew(movieId) {
const url = `https://api.themoviedb.org/3/movie/${movieId}/credits?api_key=${process.env.REACT_APP_MOVIE_API_KEY}`
try {
fetch(url)
.then((response) => response.json())
.then((data) => this.setState({ credits: data }));
} catch (err) {
console.error(err);
}
}
getDirector() {
let director = [];
if (this.state.credits !== null){
const crew = this.state.credits.crew;
let i;
for(i=0; i < crew.length; i++) {
if (crew[i].job === 'Director') {
director = crew[i];
}
}
}
this.setState({ director: director, foundDirector: true });
}
getGenres() {
const movie = this.state.movie;
let genres = [];
let i;
for (i=0; i < movie.genres.length; i++) {
genres.push(movie.genres[i]);
}
this.setState({ movieGenres: genres });
}
render() {
const movie = this.state.movie;
const cast = this.state.credits.cast;
if (cast != null && !this.state.foundDirector) {
this.getDirector();
this.getGenres();
}
const currCast = this.state.credits.cast;
const movieGenres = this.state.movieGenres;
return (
<div className="movie-details-wrapper">
<div className="details-header">
<img
className="movie-backdrop"
src={`https://image.tmdb.org/t/p/w1000_and_h450_multi_faces/${movie.backdrop_path}`} />
<h3 className="details-title">
{movie.title}
</h3>
<h5 className="details-tagling">
{movie.tagline}
</h5>
</div>
<div className="details-content">
<div className="details-director-rating">
<p className="basic-details">
Director: {this.state.director.name}
</p>
<p className="details-rating">
{movie.vote_average * 10}%
</p>
</div>
<div className="details-genres">
{movieGenres.map(genre => (
<MovieGenre genre={genre.id} />
))}
</div>
<div className="details-cast-wrapper">
<h3>Cast</h3>
<div className="details-cast">
{currCast ?
<Swiper>
{currCast.map(person => (
<SwiperSlide>
<img className="cast-img"src={`https://image.tmdb.org/t/p/w220_and_h330_bestv2/${person.profile_path}`} />
// </SwiperSlide>
))}
</Swiper>
:
<h2>No cast</h2>
}
</div>
</div>
</div>
</div>
);
}
} export default MovieDetails
--- Last Update --- Problem Solved! I think the Swiper was initialized before anything was in it so the Swiper thought it had zero slides and wouldn't function. I fixed this by adding some conditional rendering to check the length of the movies variable. Initially, this did not work with the conditional rendering because I forgot to add the length check.
Before:
<h2 className="category-title">{genreCat}</h2>
{movies ?
<Swiper>
{movies
.filter((movie) => movie.poster_path)
.map((movie) => (
<SwiperSlide key={movie.id}>
<MovieCard movie={movie}/>
</SwiperSlide>
))}
</Swiper>
:
<h2>No Movies</h2>
}
After:
<h2 className="category-title">{genreCat}</h2>
{movies.length > 0 ?
<Swiper>
{movies
.filter((movie) => movie.poster_path)
.map((movie) => (
<SwiperSlide key={movie.id}>
<MovieCard movie={movie}/>
</SwiperSlide>
))}
</Swiper>
:
<h2>No Movies</h2>
}
Problem Solved! I think the Swiper was initialized before anything was in it so the Swiper thought it had zero slides and wouldn't function. I fixed this by adding some conditional rendering to check the length of the movies variable. Initially, this did not work with the conditional rendering because I forgot to add the length check.
Before:
After: