Почему моя функция map нарушает работу компонента Swiperjs в React?

#reactjs #map-function #swiperjs

#reactjs #map-функция #swiper.js

Вопрос:

Я пытаюсь создать веб-приложение для поиска фильмов с помощью React. Я запрашиваю themoviedb.com для получения информации о фильме и отображения постера / названия фильма пользователю.

Я использую Swiperjs для отображения постеров и титров фильмов для пользователей по горизонтали. К сожалению, когда я отображаю () информацию о фильме в компонент SwiperSlide, ползунок не будет скользить. Кажется, он заикается и отказывается переходить с первого постера фильма. Может ли кто-нибудь направить меня в правильном направлении относительно того, почему это не сработает?

Как ни странно, при случайных обновлениях случайные жанры будут работать нормально до перезагрузки, но это будет делать только один жанровый компонент.

Жанровый компонент:

 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:

 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>
  )
}
  

— ОБНОВИТЬ —

Вот компонент Swiper, который я использую из 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 amp;amp; 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 amp;amp; swiperRef.current) {
        swiperRef.current.emitSlidesClasses();
        initializedRef.current = true;
      }
    });

    // watch for params change
    useIsomorphicLayoutEffect(() => {
      if (changedParams.length amp;amp; swiperRef.current amp;amp; !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 amp;amp; !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 amp;amp; 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) amp;amp; (
          <>
            <div ref={prevElRef} className="swiper-button-prev" />
            <div ref={nextElRef} className="swiper-button-next" />
          </>
        )}
        {needsScrollbar(swiperParams) amp;amp; <div ref={scrollbarElRef} className="swiper-scrollbar" />}
        {needsPagination(swiperParams) amp;amp; (
          <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}amp;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 amp;amp; !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
  

— Последнее обновление —
Проблема решена!
Я думаю, что Swiper был инициализирован до того, как в нем что-либо было, поэтому Swiper думал, что у него нулевые слайды и он не будет функционировать. Я исправил это, добавив некоторый условный рендеринг для проверки длины переменной movies. Изначально это не работало с условным отображением, потому что я забыл добавить проверку длины.

Перед:

 <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>
 }
  

После:

 <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>
     }
  

Комментарии:

1. Я просмотрел это несколько раз сегодня и не вижу ничего явно неправильного. Однако хранение реквизитов в состоянии компонента является антишаблоном. Можете ли вы предоставить свой Swiper и SwiperSlide код компонента? Можете ли вы также попробовать воспроизвести (или импортировать свой репозиторий) в работающий codesandbox, который мы можем запустить в реальном времени?

2. Я обновил свой вопрос запрошенными компонентами и ссылками, большое вам спасибо за помощь!

3. Я все еще не нашел исправления, но, возможно, я нашел что-то, что поможет? В Chrome dev tools я проверил реквизиты SwiperSlide, и он выдает его: height: 0, isBeginning: true, isEnd: true. Если вы измените размер страницы, это изменит эти свойства на то, какими они должны быть: высота: 519, isBeginning: true, isEnd: false. Я не знаю, помогает ли это или почему это происходит только тогда, когда я использую функцию map.

Ответ №1:

Проблема решена! Я думаю, что Swiper был инициализирован до того, как в нем что-либо было, поэтому Swiper думал, что у него нулевые слайды и он не будет функционировать. Я исправил это, добавив некоторый условный рендеринг для проверки длины переменной movies. Изначально это не работало с условным отображением, потому что я забыл добавить проверку длины.

Перед:

 <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>
 }
  

После:

 <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>
     }
  

Ответ №2:

Вы должны добавить «return»

 movies.map((movie) => {
 return <SwiperSlide></SwiperSlide>
}