React Snap Carousel

Карусели - отличный способ сэкономить место в пользовательском интерфейсе. Они позволяют отображать список контента, который, по сути, занимал бы значительную часть пользовательского интерфейса, в прокручиваемом по вертикали или горизонтали контейнере, не занимая при этом слишком много места на экране.

Хотя карусели бывают разных стилей и типов, создание одной из них с нуля может быть трудоемким и сложным делом. В этой статье я покажу вам, как упростить процесс с помощью React Snap Carousel - библиотеки, специально разработанной для быстрого и простого внедрения каруселей в приложениях React.

Начало работы с React Snap Carousel

Чтобы начать работу с React Snap Carousel, начните с установки пакета, используя следующую команду:

npm i react-snap-carousel

После завершения установки вы сможете интегрировать его в свои компоненты. Ниже приведен простой пример использования пакета:

import { useSnapCarousel } from 'react-snap-carousel';

 const items = Array.from({ length: 20 }).map((_, i) => ({
   id: i,
   src: https://picsum.photos/500?idx=${i}
}));

export const Carousel = () => {

   const { scrollRef, next, prev} = useSnapCarousel();

   return (
       <div>
           <div ref={scrollRef} style={{ display: 'flex', overflow: 'auto' }}>
               {items.map((item, i) => (
                   <div key={i} >
                       <img src={item.src}/>
                   </div>
               ))}
           </div>
           <button onClick={()=>prev()}>Previous</button>
           <button onClick={()=>next()}>Next</button>
       </div>
   );
}

Из примера вы можете видеть, что в React Snap Carousel нет готовых компонентов, как во многих других библиотеках carousel. Как правило, такие компоненты действуют как швейцарский армейский нож, который управляет состоянием и функциональными возможностями пакета под капотом.

Причина такого подхода заключается в том, что карусель React snap не имеет интерфейса. Это означает, что она не поставляется с готовыми компонентами или стилизованными компонентами "из коробки". Вместо этого он предоставляет единую функцию useSnapCarousel, которая обеспечивает детальный контроль над управлением состоянием и функциональностью, необходимыми для создания карусели.

Таким образом, у вас есть возможность полностью настроить визуальный дизайн и стилистику в соответствии с вашими конкретными потребностями.

Для краткости в этой статье мы будем называть React Snap Carousel "RSCC".

API-интерфейс useSnapCarousel

Функция useSnapCarousel - это пользовательский интерфейс React, который предоставляет несколько состояний, которые можно использовать для управления элементами карусели, такими как слайды, прокрутка и взаимодействия. К этим состояниям относятся:

  • pages: Это массив, представляющий все страницы или группы элементов в карусели.
  • scrollRef: Это ref объект, который прикрепляется к прокручиваемому элементу контейнера карусели.
  • gotTo: Эта функция прокручивает карусель непосредственно до указанной страницы или индекса слайда.
  • prev: Функция, которая прокручивает карусель к предыдущему индексу слайда.
  • next: Функция, которая прокручивает карусель к следующему индексу слайда.
  • refresh: Функция, которая пересчитывает и обновляет состояние карусели (например, размеры, позиции прокрутки, точки привязки)
  • hasPrevPage: Логическое значение, указывающее, доступна ли предыдущая страница для прокрутки.
  • hasNextPage: Подобно hasPrevPage, логическое значение указывает, доступна ли следующая страница для прокрутки.
  • activePageIndex: Индекс активной в данный момент страницы
  • snapPointIndexes: Массив индексов, представляющий точки привязки для элементов карусели.

На первый взгляд этот список может показаться ошеломляющим, но не волнуйтесь. Вам понадобится только часть из них - scrollRef, next, prev и refresh, чтобы создать полнофункциональную карусель, как показано в предыдущем разделе.

Чтобы вы лучше понимали, как эти функции работают вместе, мы будем использовать их шаг за шагом для создания полноценного компонента carousel.

Создание полноценной карусели

Первым шагом при создании карусели является подготовка данных для отображения. В предыдущем примере мы использовали статическую генерацию данных с помощью метода Array.from():

 const items = Array.from({ length: 20 }).map((_, i) => ({
   id: i,
   src: https://picsum.photos/500?idx=${i}
}));

Однако в реальных приложениях вы, скорее всего, будете извлекать данные асинхронно и отображать карусель, как только данные станут доступны. Чтобы продемонстрировать это, мы будем использовать Unsplash API - графический API с открытым исходным кодом - для получения списка случайных изображений для нашей карусели.

Начните с создания нового компонента в вашем проекте React. Если у вас еще не настроен проект React, ознакомьтесь с нашим руководством по настройке проекта React с помощью Vite.

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

Создайте перехватчик useFetch, добавив новый файл по адресу src/hooks/useFetch.js и добавьте следующий код:

import React from 'react';

export const useFetch = (url) => {
  const [data, setData] = React.useState();

  React.useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error(`Response status: ${response.status}`);
        }

        const result = await response.json();
        setData(result);
      } catch (error) {
        console.error(error.message);
      }
    };
    fetchData();
  }, [url]);
  return { data };
};

После создания перехватчика useFetch вернитесь к своему компоненту Carousel. Импортируйте перехватчик и передайте конечную точку Unsplash API в качестве аргумента. Затем преобразуйте состояние данных из возвращаемых значений перехватчика:

import { useFetch } from '../hooks/useFetch';

const Carousel = () => {
  const url = `https://api.unsplash.com/photos/random?client_id=${import.meta.env.VITE_API_KEY}&count=10`;

  const { data, isLoading, error } = useFetch(url);

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <div>
      ...
    </div>
  );
};

Примечание: замените переменную среды VITE_API_KEY на ваш ключ доступа к Unsplash API.

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

Example Of Successful Fetched Image In React Carousel

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

import { useFetch } from '../hooks/useFetch';
import { useSnapCarousel } from 'react-snap-carousel';

const Carousel = () => {
  const {
    scrollRef,
    pages,
    goTo,
    prev,
    next,
    activePageIndex,
    hasPrevPage,
    hasNextPage,
    snapPointIndexes,
    refresh
  } = useSnapCarousel();

  ...

  return (
    <div>
      ...
    </div>
  );
};

Создание контейнера с возможностью прокрутки

Для отображения элементов карусели нам нужен прокручиваемый контейнер, в котором элементы будут отображаться. Мы прикрепим объект scrollRef из RSC к этому контейнеру, чтобы включить функцию прокрутки:

    <div>
      <ul ref={scrollRef}>
        {data?.map((item, i) => (
          <img
            src={item.urls.small}
            width="250"
            height="250"
            alt={item.alt_description}
          />
        ))}
      </ul>
    </div>

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

Помните, что RSC не имеет заголовка и не предоставляет стили по умолчанию, поэтому нам нужно будет определить CSS, чтобы карусель выглядела так, как задумано.

Добавьте следующий CSS-код для оформления контейнера и элементов. Вы можете включить его на верхнем уровне компонента Carousel или в отдельный CSS-файл (например, Carousel.css) и импортировать.:

const styles = {
   container: {
       position: 'relative',
       display: 'flex',
       overflow: 'auto',
       scrollSnapType: 'x mandatory',
       scrollBehavior: 'smooth',
   },
   item: {
       width: '350px',
       height: '450px',
       listStyleType: 'none',
       flexShrink: 0,
   },
   img: {
       width: '100%',
       height: '100%',
       objectFit: 'cover',
   },
   buttonDisabled: {opacity: 0.3},
   activeIndex: {opacity: 0.3},
   controls: {
       display: 'flex',
       justifyContent: 'center',
       alignItems: 'center',
       margin: '10px',
   },
   itemSnapPoint: {
       scrollSnapAlign: 'start',
   },
};

Другие компоненты карусели еще не добавлены, поэтому мы пока не можем увидеть эффекты стиля, но мы можем предварительно изменить размер и расположить элементы по горизонтали, передав свойства контейнера и элемента в атрибут стиля для элементов ul и li соответственно:

      <ul style={styles.container} ref={scrollRef}>
        {data?.map((item, i) => (
          <li key={item.id} style={styles.item}>
        ...
          </li>
        ))}
      </ul>

На данном этапе вы увидите это:

Example Of Pictures In React Carousel

Имейте в виду, что размер фрагментов карусели может отличаться из-за различий в размерах изображений. Мы можем решить эту проблему с помощью CSS-стилей, как в примере выше. В частности, мы будем настраивать параметры выбора элементов и изображений:

item: {
   width: '350px',
   height: '450px',
   listStyleType: 'none',
   flexShrink: 0,
},
img: {
   width: '100%',
   height: '100%',
   objectFit: 'cover',
},

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

Добавление элементов управления

На следующем шаге мы добавим элементы управления в карусель, используя функции next и prev. Вставьте следующий код под элементом container.:

<button onClick={()=>prev()}>{String.fromCharCode(8592)}</button>
<button onClick={()=>next()}>{String.fromCharCode(8594)}</button>

Любая из функций будет вызвана в зависимости от того, какая кнопка нажата. Статические методы String.fromCharCode() будут отображать значки стрелок, используя последовательность предоставленного кода в Юникоде.

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

Controls Not Working In React Carousel

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

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

Добавьте объявление useEffect в компонент Carousel. Внутри него вызовите функцию обновления. Включите данные и состояния scrollRef в массив зависимостей:

useEffect(() => {
   refresh();
}, [data, scrollRef]);

Теперь, если вы вернетесь в браузер и будете взаимодействовать с каруселью, она должна работать должным образом.

Controls Working In React Carousel

Пагинация

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

Мы вставим разбивку на страницы между кнопками prev и next внутри оболочки элементов управления:

<div style={styles.controls}>
   <button onClick={() => prev()}>{String.fromCharCode(8592)}</button>
   <div>
       {pages.map((_, i) => (
           <button key={i}>{i + 1}</button>
       ))}
   </div>
   <button onClick={()=>next()}>{String.fromCharCode(8594)}</button>
</div>

Чтобы сделать каждый номер разбивки на страницы интерактивным и включить прокрутку до соответствующего элемента карусели, добавьте событие щелчка к номерам с помощью функции перехода:

<div style={styles.controls}>
    ...

       {pages.map((_, i) => (
           <button key={i} onClick={() => goTo(i)}>{i + 1}</button>
       ))}

    ...
</div>

При нажатии на номер разбивки на страницы его индекс передается в качестве аргумента функции goTo. Этот индекс определяет слайд, к которому нужно перейти.

Carousel scrolling


Далее мы будем использовать состояния hasPrevPage и hasNextPage, чтобы проверить, находится ли карусель в начале или в конце своего содержимого, а затем отключим кнопки prev или next соответственно:

<button
   onClick={() => prev()}
   disabled={!hasPrevPage}
   style={!hasPrevPage ? styles.buttonDisabled : {}}
>
   ...
</button>

<button
   onClick={() => next()}
   disabled={!hasNextPage}
   style={!hasNextPage ? styles.buttonDisabled : {}}
>
   ...
</button>

Свойство стиля buttonDisabled аналогично уменьшает непрозрачность кнопок, делая очевидным, что кнопка отключена, когда любое из состояний возвращает истинное значение.

Аналогично, мы будем использовать activePageIndex state для определения текущей видимой страницы и условно стилизуем ее, чтобы выделить индикатор активной страницы:

<div>
   {pages.map((_, i) => (
       <button
           key={i}
           onClick={() => goTo(i)}
           style={activePageIndex === i ? styles.activeIndex : {}}
       >
           {i + 1}
       </button>
   ))}
</div>

Свойство стиля activeIndex также уменьшает непрозрачность индикатора активной страницы.

Настройка точек привязки

Последним в списке находится snapPointIndexes. Нам не нужно много делать с этим приложением, поскольку оно сохраняет только индексы элементов в карусели, которые действуют как точки привязки (предопределенные позиции в карусели, в которых прокрутка прекращается или привязывается).

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

Но в нашем коде все, для чего нам нужно состояние snapPointIndexes, - это задать поведение привязки отдельных элементов в прокручиваемом контейнере следующим образом:

{data?.map((item, i) => (
   <li
       key={`${item.id}-${i}`}
       style={{
           ...styles.item,
           ...(snapPointIndexes.has(i) ? styles.itemSnapPoint : {})
       }}
   >
    ...
   </li>
))}

Вот правило для селектора itemSnapPoint:

itemSnapPoint: {
   scrollSnapAlign: 'start',
},

Правило scrollSnapAlign: Start определяет, что начальный край элемента (левый край для горизонтальной прокрутки) должен совпадать с точкой привязки контейнера, когда происходит привязка.

Благодаря этой настройке у вас теперь есть карусель с полнофункциональными элементами управления и разбивкой на страницы. Далее мы рассмотрим возможность расширения функциональности и добавления бесконечной прокрутки.

Реализация функции бесконечной прокрутки

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

Основная идея пользовательского управления этой функциональностью заключается в циклическом просмотре содержимого и управлении поведением прокрутки. Вот пошаговый подход к тому, как мы собираемся этого добиться.:

  • Пользовательские обработчики: Первым шагом является создание пользовательских обработчиков вместо функций prev и next. Таким образом, мы можем создать пользовательскую логику для нашей реализации Отслеживание положения прокрутки: Далее мы используем состояния для отслеживания того, когда прокрутка достигает начала или конца списка Сброс положения прокрутки: Наконец, мы быстро возвращаемся к началу или концу, когда пользователь переходит к последнему или первому элементу в списке

Во-первых, мы удалим из нашего кода следующие элементы:

  • Функции hasPrevPage и hasNextPage Отключенные атрибуты на кнопках управления

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

После удаления этих элементов мы можем приступить к реализации пользовательских обработчиков:

const handleNext = useCallback(() => {
   if (activePageIndex === pages.length - 1) {
       goTo(0);
   } else {
       next();
   }
}, [activePageIndex, pages.length, goTo, next]);

const handlePrev = useCallback(() => {
   if (activePageIndex === 0) {
       goTo(pages.length - 1);
   } else {
       prev();
   }
}, [activePageIndex, pages.length, goTo, prev]);

Как вы можете видеть, мы не полностью избавляемся от функций prev и next. Вместо этого мы используем функции activePageIndex, pages и goTo, чтобы определить, находится ли карусель в начале или в конце. Если это так, мы плавно перейдем к противоположному концу. В противном случае мы вызовем предыдущую или следующую функцию как обычно.

Наконец, мы присоединим эти пользовательские обработчики к событию onClick кнопок управления:

<div style={styles.controls}>   
   <button onClick={handlePrev}>
       ...
   </button>
    ...
   <button onClick={handleNext}>
       ...
   </button>
</div>

Благодаря этому мы успешно создали иллюзию бесконечного прокручивания нашей карусели, несмотря на ограничения.

Carousel product in React


Вот полный код для компонента Carousel:

import {useSnapCarousel} from 'react-snap-carousel';
import {useFetch} from "../hooks/useFetch.js";
import {useEffect, useCallback} from "react";

export const Carousel_test = () => {
   const url = `https://api.unsplash.com/photos/random?client_id=${
       import.meta.env.VITE_API_KEY
   }&count=20`;

   const {data, isLoading, error} = useFetch(url);

   const {
       scrollRef,
       pages,
       goTo,
       prev,
       next,
       activePageIndex,
       snapPointIndexes,
       refresh,
   } = useSnapCarousel();

   const handleNext = useCallback(() => {
       if (activePageIndex === pages.length - 1) {
           goTo(0);
       } else {
           next();
       }
   }, [activePageIndex, pages.length, goTo, next]);

   const handlePrev = useCallback(() => {
       if (activePageIndex === 0) {
           goTo(pages.length - 1);
       } else {
           prev();
       }
   }, [activePageIndex, pages.length, goTo, prev]);

   useEffect(() => {
       refresh();
   }, [data, scrollRef]);

   if (isLoading) return <div>Loading...</div>;
   if (error) return <div>Error: {error}</div>;

   return (
       <div>
           <ul style={styles.container} ref={scrollRef}>
               {data?.map((item, i) => (
                   <li
                       key={`${item.id}-${i}`}
                       style={{
                           ...styles.item,
                           ...(snapPointIndexes.has(i) ? styles.itemSnapPoint : {})
                       }}
                   >
                       <img
                           style={styles.img}
                           src={item.urls.small}
                           alt={item.alt_description}
                       />
                   </li>
               ))}
           </ul>
           <div style={styles.controls}>
               <button onClick={handlePrev}>
                   {String.fromCharCode(8592)}
               </button>
               <div>
                   {pages.map((_, i) => (
                       <button
                           key={i}
                           onClick={() => goTo(i)}
                           style={activePageIndex === i ? styles.activeIndex : {}}
                       >
                           {i + 1}
                       </button>
                   ))}
               </div>
               <button onClick={handleNext}>
                   {String.fromCharCode(8594)}
               </button>
           </div>
       </div>
   );
};

const styles = {
   container: {
       position: 'relative',
       display: 'flex',
       overflow: 'auto',
       scrollSnapType: 'x mandatory',
       scrollBehavior: 'smooth',
   },
   item: {
       width: '350px',
       height: '450px',
       listStyleType: 'none',
       flexShrink: 0,
   },
   img: {
       width: '100%',
       height: '100%',
       objectFit: 'cover',
   },
   buttonDisabled: {opacity: 0.3},
   activeIndex: {opacity: 0.3},
   controls: {
       display: 'flex',
       justifyContent: 'center',
       alignItems: 'center',
       margin: '10px',
   },
   itemSnapPoint: {
       scrollSnapAlign: 'start',
   },
};

Вывод

Пакет React Scroll Carousel, без сомнения, является отличным выбором для создания гибких и настраиваемых каруселей. И, учитывая его "безголовый" характер, он позволяет вам свободно адаптировать дизайн вашей карусели к вашим конкретным потребностям.

Хотя у пакета есть свои ограничения, как мы видели в статье, идея расширения его функциональности не является надуманной и может быть реализована при наличии основательных знаний JavaScript и React.