Как стать автором
Обновить
2107.27

React: решение интересной практической задачи

Время на прочтение15 мин
Количество просмотров8.9K


Привет, друзья!


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


Предположим, что у нас имеется страница сравнения товаров. На этой странице отображается слайдер с карточками товаров и таблица с их характеристиками. Задача состоит в том, чтобы синхронизировать переключение слайдов и прокрутку таблицы. Условия следующие:


  • ширина таблицы должна соответствовать ширине слайдера;
  • ширина колонки таблицы должна соответствовать ширине слайда;
  • слайды можно переключать с помощью перетаскивания, нажатия на кнопки управления и элементы пагинации;
  • таблицу можно прокручивать с помощью колесика мыши (на десктопе) и перемещения указателя (на телефоне);
  • при взаимодействии пользователя с одним компонентом второй должен реагировать соответствующим образом: при переключении слайда должна выполняться прокрутка таблицы, при прокрутке таблицы — переключение слайдов.

Репозиторий с кодом проекта.


Если вам это интересно, прошу под кат.


Подготовка и настройка проекта


Для работы с зависимостями я буду использовать Yarn. Проект будет реализован на React и TypeScript.


Создаем шаблон проекта с помощью Vite:


# react-slider-table - название проекта
# react-ts - используемый шаблон
yarn create vite react-slider-table --template react-ts

Переходим в созданную директорию, устанавливаем зависимости и запускаем сервер для разработки:


cd react-slider-table
yarn
yarn dev

Для реализации слайдера будет использоваться библиотека Swiper (для синхронизации слайдера и таблицы мы будем использовать некоторые возможности, предоставляемые Swiper, поэтому в рамках туториала рекомендую использовать именно эту библиотеку). Устанавливаем ее:


yarn add swiper
yarn add -D @types/swiper

Импортируем стили слайдера в файле main.tsx:


import "swiper/css";
// для модулей навигации и пагинации
import "swiper/css/navigation";
import "swiper/css/pagination";

Определяем минимальные стили в файле index.css (файл App.css можно удалить):


@import url("https://fonts.googleapis.com/css2?family=Montserrat&display=swap");

* {
  font-family: "Montserrat", sans-serif;
}

body {
  margin: 0;
}

.app {
  margin: 0 auto;
  padding: 1rem;
  width: 768px;
}

img {
  max-width: 100%;
  object-fit: cover;
}

.table-wrapper {
  overflow: scroll;
  scrollbar-width: none;
}

.table-wrapper::-webkit-scrollbar {
  display: none;
}

table {
  border-collapse: collapse;
  overflow: hidden;
}

td {
  border: 1px solid gray;
  padding: 0.25rem;
  text-align: center;
}

.feature-name-row td {
  font-weight: bold;
  text-align: left;
}

.feature-name {
  position: relative;
}

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


Создаем файл types.ts следующего содержания:


export type Feature = {
  id: number;
  value: string;
};

export type Item = {
  id: number;
  title: string;
  imageUrl: string;
  price: number;
  features: Feature[];
};

export type Items = Item[];

Создаем файл data.ts следующего содержания:


import { Items } from "./types";

const items: Items = [
  {
    id: 1,
    title: "Title",
    imageUrl: `https://picsum.photos/320?random=${Math.random()}`,
    price: 100,
    features: [
      {
        id: 1,
        value: "Feature",
      },
      {
        id: 2,
        value: "Feature2",
      },
      {
        id: 3,
        value: "Feature3",
      },
      {
        id: 4,
        value: "Feature4",
      },
      {
        id: 5,
        value: "Feature5",
      },
      {
        id: 6,
        value: "Feature6",
      },
    ],
  },
  {
    id: 2,
    title: "Title2",
    imageUrl: `https://picsum.photos/320?random=${Math.random()}`,
    price: 200,
    features: [
      {
        id: 1,
        value: "Feature7",
      },
      {
        id: 2,
        value: "Feature8",
      },
      {
        id: 3,
        value: "Feature9",
      },
      {
        id: 4,
        value: "Feature10",
      },
      {
        id: 5,
        value: "Feature11",
      },
      {
        id: 6,
        value: "Feature12",
      },
    ],
  },
  {
    id: 3,
    title: "Title3",
    imageUrl: `https://picsum.photos/320?random=${Math.random()}`,
    price: 300,
    features: [
      {
        id: 1,
        value: "Feature13",
      },
      {
        id: 2,
        value: "Feature14",
      },
      {
        id: 3,
        value: "Feature15",
      },
      {
        id: 4,
        value: "Feature16",
      },
      {
        id: 5,
        value: "Feature17",
      },
      {
        id: 6,
        value: "Feature18",
      },
    ],
  },
  {
    id: 4,
    title: "Title4",
    imageUrl: `https://picsum.photos/320?random=${Math.random()}`,
    price: 400,
    features: [
      {
        id: 1,
        value: "Feature19",
      },
      {
        id: 2,
        value: "Feature20",
      },
      {
        id: 3,
        value: "Feature21",
      },
      {
        id: 4,
        value: "Feature22",
      },
      {
        id: 5,
        value: "Feature23",
      },
      {
        id: 6,
        value: "Feature24",
      },
    ],
  },
  {
    id: 5,
    title: "Title5",
    imageUrl: `https://picsum.photos/320?random=${Math.random()}`,
    price: 500,
    features: [
      {
        id: 1,
        value: "Feature25",
      },
      {
        id: 2,
        value: "Feature26",
      },
      {
        id: 3,
        value: "Feature27",
      },
      {
        id: 4,
        value: "Feature28",
      },
      {
        id: 5,
        value: "Feature29",
      },
      {
        id: 6,
        value: "Feature30",
      },
    ],
  },
  {
    id: 6,
    title: "Title6",
    imageUrl: `https://picsum.photos/320?random=${Math.random()}`,
    price: 600,
    features: [
      {
        id: 1,
        value: "Feature31",
      },
      {
        id: 2,
        value: "Feature32",
      },
      {
        id: 3,
        value: "Feature33",
      },
      {
        id: 4,
        value: "Feature34",
      },
      {
        id: 5,
        value: "Feature35",
      },
      {
        id: 6,
        value: "Feature36",
      },
    ],
  },
];

export default items;

У нас имеется массив, содержащий 6 объектов с информацией о товарах. Каждый объект товара содержит массив, состоящий из 6 объектов с характеристиками товара.


Создаем директорию components.


Начнем с разработки слайдера. Создаем файл components/Slider.tsx следующего содержания:


// модули
import { Navigation, Pagination } from "swiper";
// компоненты
import { Swiper, SwiperSlide } from "swiper/react";
import { Items } from "../types";

type Props = {
  items: Items;
};

// количество отображаемых слайдов
const SLIDES_PER_VIEW = 3;

function Slider({ items }: Props) {
  return (
    <Swiper
      // подключаем модули навигации и пагинации
      modules={[Navigation, Pagination]}
      // индикатор отображения навигации
      navigation={SLIDES_PER_VIEW < items.length}
      // индикатор отображения пагинации
      pagination={
        SLIDES_PER_VIEW < items.length
          ? {
              // элементы пагинации должны быть кликабельными
              clickable: true,
            }
          : undefined
      }
      // количество отображаемых слайдов
      slidesPerView={SLIDES_PER_VIEW}
    >
      {items.map((item) => (
        <SwiperSlide key={item.id}>
          <img src={item.imageUrl} alt={item.title} />
          <div>
            <h2>{item.title}</h2>
            <p>{item.price} ₽</p>
          </div>
        </SwiperSlide>
      ))}
    </Swiper>
  );
}

export default Slider;

Импортируем и рендерим слайдер в файле App.tsx:


import Slider from "./components/Slider";
import data from "./data";

function App() {
  return (
    <div className="app">
      <Slider items={data} />
    </div>
  );
}

export default App;

Результат:





Теперь реализуем компонент таблицы. Создаем файл components/Table.tsx следующего содержания:


import { Items } from "../types";

type Props = {
  items: Items;
};

// названия характеристик
const FEATURE_NAMES = [
  "Title",
  "Title2",
  "Title3",
  "Title4",
  "Title5",
  "Title6",
];

function Table({ items }: Props) {
  return (
    <div className="table-wrapper">
      <table>
        <tbody>
          {items.map((item, i) => (
            <React.Fragment key={item.id}>
              <tr className="feature-name-row">
                <td colSpan={items.length}>
                  <span className="feature-name">{FEATURE_NAMES[i]}</span>
                </td>
              </tr>
              <tr>
                {items.map((_, j) => {
                  const key = "" + i + j;
                  return <td key={key}>{items[j].features[i].value}</td>;
                })}
              </tr>
            </React.Fragment>
          ))}
        </tbody>
      </table>
    </div>
  );
}

export default Table;

Обратите внимание на 2 вещи:


  • мы оборачиваем таблицу с overflow: hidden в контейнер с overflow: scroll (.table-wrapper);
  • колонка с названием характеристики растягивается на всю ширину таблицы по количеству товаров (атрибут colspan), а само название оборачивается в элемент span: при прокрутке таблицы название характеристики должно оставаться видимым.

Импортируем и рендерим таблицу в App.tsx:


import Slider from "./components/Slider";
import Table from "./components/Table";
import data from "./data";

function App() {
  return (
    <div className="app">
      <Slider items={data} />
      <Table items={data} />
    </div>
  );
}

export default App;

Результат:





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


Синхронизация ширины слайда и колонки таблицы


Определяем состояние ширины слайда в App.tsx:


const [slideWidth, setSlideWidth] = useState(0);

Данное состояние будет обновляться в слайдере, а использоваться — в таблице:


<Slider
  items={data}
  // !
  setSlideWidth={setSlideWidth}
/>
<Table
  items={data}
  // !
  slideWidth={slideWidth}
/>

Определяем переменную для хранения ссылки на экземпляр Swiper в Slider.tsx:


const swiperRef = useRef<TSwiper>();

Тип TSwiper выглядит так:


// types.ts
import type Swiper from "swiper";

export type TSwiper = Swiper & {
  slides: {
    swiperSlideSize: number;
  }[];
};

Одним из пропов, принимаемых компонентом Swiper, является onSwiper. В качестве аргумента коллбэку этого пропа передается экземпляр Swiper:


<Swiper
  onSwiper={(swiper) => {
    console.log(swiper);

    swiperRef.current = swiper as TSwiper;
  }}
  // ...
>

Экземпляр Swiper содержит массу полезных свойств:





Интересующее нас значение ширины слайда содержится в свойстве slides[0].swiperSlideSize:





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


<Swiper
  onSwiper={(swiper) => {
    console.log(swiper);

    swiperRef.current = swiper as TSwiper;
  }}
  onImagesReady={onImagesReady}
  // ...
>

Определяем функцию onImagesReady:


const onImagesReady = () => {
  if (!swiperRef.current) return;

  const slideWidth = swiperRef.current.slides[0].swiperSlideSize;
  setSlideWidth(slideWidth);
};

Применяем проп slideWidth в таблице с помощью встроенных стилей (в реальном приложении для этого, скорее всего, будет использоваться одно из решений CSS-in-JS, например, styled-jsx — см. конец статьи):


<tr
  // !
  style={{
    display: "grid",
    // 6 колонок с шириной, равной ширине слайда
    gridTemplateColumns: `repeat(${items.length}, ${slideWidth}px)`,
  }}
>
  {items.map((_, j) => {
    const key = "" + i + j;
    return <td key={key}>{items[j].features[i].value}</td>;
  })}
</tr>

Результат:





Синхронизация переключения слайдов и прокрутки таблицы: обработка переключения слайдов


Определяем состояние прокрутки в App.tsx:


const [scrollLeft, setScrollLeft] = useState(0);

Данное состояние, как и состояние ширины слайда, будет обновляться в слайдере, а использоваться — в таблице:


<Slider
  items={data}
  setSlideWidth={setSlideWidth}
  // !
  setScrollLeft={setScrollLeft}
/>
<Table
  items={data}
  slideWidth={slideWidth}
  // !
  scrollLeft={scrollLeft}
/>

Проп onSlideChange компонента Swiper принимает коллбэк, позволяющий выполнять операции после переключения слайдов (любым способом):


<Swiper
  onSlideChange={onSlideChange}
  // ...
>

Прежде чем определять функцию onSlideChange, взглянем на то, что происходит с элементом div с классом swiper-wrapper при переключении слайдов:





Видим, что к данному элементу применяется встроенный стиль transform: translate3d(x, y, z), где x — интересующее нас значение прокрутки.


Функция onSlideChange выглядит следующим образом:


const onSlideChange = () => {
  if (!swiperRef.current) return;

  // извлекаем значение свойства `transform`
  const { transform } = swiperRef.current.wrapperEl.style;
  // извлекаем значение координаты `x`
  const match = transform.match(/-?\d+(\.\d+)?px/);
  if (!match) return;

  // извлекаем положительное (!) число из значения координаты `x`
  // с числами работать удобнее, чем со строками
  const scrollLeft = Math.abs(Number(match[0].replace("px", "")));
  setScrollLeft(scrollLeft);
};

Для того, чтобы применить проп scrollLeft в таблице, необходимо сделать несколько вещей.


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


const tableWrapperRef = useRef<HTMLDivElement | null>(null);
const tableRef = useRef<HTMLTableElement | null>(null);
const featureNameRefs = useRef<HTMLSpanElement[]>([]);

Передаем ссылки соответствующим элементам:


<div
  className="table-wrapper"
  // !
  ref={tableWrapperRef}
>
  <table
    // !
    ref={tableRef}
  >
    {/* ... */}
  </table>
</div>

Собираем ссылки на элементы с названиями характеристик после рендеринга компонента:


useEffect(() => {
  if (!tableRef.current) return;

  featureNameRefs.current = [
    ...tableRef.current.querySelectorAll(".feature-name"),
  ] as HTMLSpanElement[];
}, []);

Наконец, выполняем прокрутку таблицы и сдвиг по оси x названий характеристик при изменении значения scrollLeft:


useEffect(() => {
  if (!tableWrapperRef.current || !featureNameRefs.current.length) return;

  tableWrapperRef.current.scrollLeft = scrollLeft;

  featureNameRefs.current.forEach((el) => {
    el.style.left = `${scrollLeft}px`;
  });
}, [scrollLeft]);

Результат:





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


Синхронизация переключения слайдов и прокрутки таблицы: обработка прокрутки таблицы


Определяем состояние отступа по оси x в App.tsx:


const [offsetX, setOffsetX] = useState(0);

Данное состояние будет обновляться в таблице, а использоваться — в слайдере:


<Slider
  items={data}
  setSlideWidth={setSlideWidth}
  setScrollLeft={setScrollLeft}
  // !
  offsetX={offsetX}
/>
<Table
  items={data}
  slideWidth={slideWidth}
  scrollLeft={scrollLeft}
  // !
  setOffsetX={setOffsetX}
/>

Как при прокрутке таблицы с помощью колесика мыши, так и с помощью перемещения указателя, на обертке для таблицы возникает событие scroll:


<div
  className="table-wrapper"
  // !
  onScroll={debouncedOnScroll}
  ref={tableWrapperRef}
>

Определяем функцию onScroll:


const onScroll: React.UIEventHandler<HTMLDivElement> = useCallback(() => {
  if (!tableRef.current) return;
  // извлекаем позицию левого края таблицы по оси `x`
  const { x } = tableRef.current.getBoundingClientRect();
  // делаем число положительным
  setOffsetX(Math.abs(x));
}, []);

Обратите внимание: обработка прокрутки должна выполняться с задержкой, поскольку установка свойства scrollLeft приводит к возникновению события scroll, что может заблокировать переключение слайдов и прокрутку таблицы:


  • offsetX передается в слайдер и используется для переключения слайдов;
  • в обработчике переключения слайдов происходит обновление scrollLeft;
  • scrollLeft используется для выполнения прокрутки таблицы — возникает событие scroll, в обработчике которого обновляется offsetX.

Также обратите внимание, что прокрутка должна выполняться мгновенно: установка стиля scroll-behavior: smooth или выполнение прокрутки с помощью метода scrollTo({ left: scrollLeft, behavior: 'smooth' }) сделает поведение прокрутки непредсказуемым.


Создаем файл hooks/useDebounce.ts следующего содержания:


import { useCallback, useEffect, useRef } from "react";

const useDebounce = (fn: Function, delay: number) => {
  const timeoutRef = useRef<number>();

  const clearTimer = useCallback(() => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
      timeoutRef.current = undefined;
    }
  }, []);

  useEffect(() => clearTimer, []);

  const cb = useCallback(
    (...args: any[]) => {
      clearTimer();
      timeoutRef.current = setTimeout(() => fn(...args), delay);
    },
    [fn, delay]
  );

  return cb;
};

export default useDebounce;

Этот хук возвращает функцию, которая, независимо от количества запусков, вызывается только один раз по прошествии указанного времени:


const ON_SCROLL_DELAY = 250;

const debouncedOnScroll = useDebounce(onScroll, ON_SCROLL_DELAY);

Переходим к самой сложной части туториала.


Применение пропа offsetX в слайдере предполагает знание количества элементов пагинации, определение ближайшего к offsetX элемента и его программное нажатие.


Определяем переменные для хранения ссылок на элементы пагинации и их позиции по оси x:


const paginationBulletRefs = useRef<HTMLSpanElement[]>([]);
const paginationBulletXCoords = useRef<number[]>([]);

Ссылки на элементы пагинации хранятся в свойстве pagination.bullets экземпляра Swiper. Для определения позиций элементов по оси x достаточно умножить индекс элемента на ширину слайда. Расширяем функцию onImagesReady:


const bullets = swiperRef.current.pagination
  .bullets as unknown as HTMLSpanElement[];
if (!bullets.length) return;
paginationBulletRefs.current = bullets;

for (const i in bullets) {
  paginationBulletXCoords.current.push(slideWidth * Number(i));
}

Определяем эффект для выполнения программного нажатия на соответствующий элемент пагинации при изменении offsetX:


useEffect(() => {
  // переменная для минимальной разницы между позицией элемента и отступом
  let min = 0;
  let i = 0;

  for (const j in paginationBulletXCoords.current) {
    // вычисляем текущую разницу
    const dif = Math.abs(paginationBulletXCoords.current[j] - offsetX);

    // текущая разница равна `0`
    if (dif === 0) {
      min = 0;
      i = 0;
      break;
    }

    // текущая разница не равна `0` и минимальная разница равна `0` или текущая разница меньше минимальной разницы
    if (dif !== 0 && (min === 0 || dif < min)) {
      min = dif;
      i = Number(j);
    }
  }

  // выполняем программное нажатие на соответствующий элемент
  if (paginationBulletRefs.current[i]) {
    paginationBulletRefs.current[i].click();
  }
}, [offsetX]);

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


Результат:





Видим, что прокрутка таблицы с помощью колесика мыши или перемещения указателя приводит сначала к переключению слайда, а затем — к выравниванию таблицы и названий характеристик.


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


Обновление стилей в таблице можно упростить с помощью одного из решений CSS-in-JS, а именно: styled-jsx. Устанавливаем эту библиотеку:


yarn add styled-jsx
yarn add -D @types/styled-jsx

Редактируем файл vite.config.ts:


import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [
    react({
      babel: { plugins: ["styled-jsx/babel"] },
    }),
  ],
});

Редактируем файл vite-env.d.ts:


/// <reference types="vite/client" />
import "react";

declare module "react" {
  interface StyleHTMLAttributes {
    jsx?: boolean;
    global?: boolean;
  }
}

Наконец, редактируем файл Table.tsx:


import React, { useCallback, useEffect, useRef } from "react";
import useDebounce from "../hooks/useDebounce";
import { Items } from "../types";

type Props = {
  items: Items;
  slideWidth: number;
  scrollLeft: number;
  setOffsetX: React.Dispatch<React.SetStateAction<number>>;
};

const FEATURE_NAMES = [
  "Title",
  "Title2",
  "Title3",
  "Title4",
  "Title5",
  "Title6",
];

const ON_SCROLL_DELAY = 250;

function Table({ items, slideWidth, scrollLeft, setOffsetX }: Props) {
  const tableWrapperRef = useRef<HTMLDivElement | null>(null);
  const tableRef = useRef<HTMLTableElement | null>(null);

  useEffect(() => {
    if (!tableWrapperRef.current) return;

    tableWrapperRef.current.scrollLeft = scrollLeft;
  }, [scrollLeft]);

  const onScroll: React.UIEventHandler<HTMLDivElement> = useCallback(() => {
    if (!tableRef.current) return;

    const { x } = tableRef.current.getBoundingClientRect();
    setOffsetX(Math.abs(x));
  }, []);

  const debouncedOnScroll = useDebounce(onScroll, ON_SCROLL_DELAY);

  return (
    <>
      <div
        className="table-wrapper"
        onScroll={debouncedOnScroll}
        ref={tableWrapperRef}
      >
        <table ref={tableRef}>
          <tbody>
            {items.map((item, i) => (
              <React.Fragment key={item.id}>
                <tr className="feature-name-row">
                  <td colSpan={items.length}>
                    <span className="feature-name">{FEATURE_NAMES[i]}</span>
                  </td>
                </tr>
                {/* ! */}
                <tr className="feature-value-row">
                  {items.map((_, j) => {
                    const key = "" + i + j;
                    return <td key={key}>{items[j].features[i].value}</td>;
                  })}
                </tr>
              </React.Fragment>
            ))}
          </tbody>
        </table>
      </div>
      {/* ! */}
      <style jsx>{`
        .feature-name {
          left: ${scrollLeft}px;
        }
        .feature-value-row {
          display: grid;
          grid-template-columns: repeat(${items.length}, ${slideWidth}px);
        }
      `}</style>
    </>
  );
}

export default Table;

Мы также можем отрефакторить код слайдера, упростив процесс переключения слайдов в ответ на прокрутку таблицы. Экземпляр Swiper предоставляет метод slideTo, позволяющий программно прокручивать слайдер к указанному слайду. Следовательно, вместо позиций элементов пагинации по оси x нам необходимо знать позиции слайдов. Эти позиции содержатся в свойстве slidesGrid. Кроме того, как верно отметил yavoloh в комментарии, смещение слайдера по оси x можно извлекать из свойства translate. Редактируем файл Slider.tsx:


import { useEffect, useRef } from "react";
import { Navigation, Pagination } from "swiper";
import { Swiper, SwiperSlide } from "swiper/react";
import { Items, TSwiper } from "../types";

type Props = {
  items: Items;
  setSlideWidth: React.Dispatch<React.SetStateAction<number>>;
  setScrollLeft: React.Dispatch<React.SetStateAction<number>>;
  offsetX: number;
};

const SLIDES_PER_VIEW = 3;

function Slider({ items, setSlideWidth, setScrollLeft, offsetX }: Props) {
  const swiperRef = useRef<TSwiper>();
  // !
  const slidesGrid = useRef<number[]>([]);

  const onImagesReady = () => {
    if (!swiperRef.current) return;

    const slideWidth = swiperRef.current.slides[0].swiperSlideSize;

    // !
    slidesGrid.current = swiperRef.current.slidesGrid;

    setSlideWidth(slideWidth);
  };

  const onSlideChange = () => {
    if (!swiperRef.current) return;

    // !
    const scrollLeft = Math.abs(swiperRef.current.translate);
    setScrollLeft(scrollLeft);
  };

  useEffect(() => {
    if (!swiperRef.current) return;

    let min = 0;
    let i = 0;

    for (const j in slidesGrid.current) {
      const dif = Math.abs(slidesGrid.current[j] - offsetX);

      if (dif === 0) {
        min = 0;
        i = 0;
        break;
      }

      if (dif !== 0 && (min === 0 || dif < min)) {
        min = dif;
        i = Number(j);
      }
    }

    // !
    if (items[i]) {
      swiperRef.current.slideTo(i);
    }
  }, [offsetX]);

  return (
    <Swiper
      onSwiper={(swiper) => {
        console.log(swiper);

        swiperRef.current = swiper as TSwiper;
      }}
      modules={[Navigation, Pagination]}
      navigation={SLIDES_PER_VIEW < items.length}
      onImagesReady={onImagesReady}
      onSlideChange={onSlideChange}
      pagination={
        SLIDES_PER_VIEW < items.length
          ? {
              clickable: true,
            }
          : undefined
      }
      slidesPerView={SLIDES_PER_VIEW}
    >
      {items.map((item) => (
        <SwiperSlide key={item.id}>
          <img src={item.imageUrl} alt={item.title} />
          <div>
            <h2>{item.title}</h2>
            <p>{item.price} ₽</p>
          </div>
        </SwiperSlide>
      ))}
    </Swiper>
  );
}

export default Slider;

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


Надеюсь, вы узнали что-то новое и не зря потратили время.


Благодарю за внимание и happy coding!




Теги:
Хабы:
Всего голосов 14: ↑13 и ↓1+12
Комментарии6

Публикации

Информация

Сайт
timeweb.cloud
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия
Представитель
Timeweb Cloud