React でスワイプによるスライドの操作を実装するライブラリ react-swipeable-views が非推奨になりました。この記事では、MUI (旧 Material-UI) のドキュメントに掲載されている react-swipeable-views を使ったコードを代替するライブラリ Swiper に置き換えていきます。

先行記事

【React】Swiper + MUI Tab を利用してスワイプでタブ切り替えを行う
https://ramble.impl.co.jp/1959/

本記事では以上の記事を大いに参考にしていますが、以下の2点で異なります。

  1. TypeScript で記述
  2. react-swipeable-views からの移行を念頭に置いて、その差分を記述

react-swipeable-views

react-swipeable-views は React でスワイプ動作を実装するライブラリです。

MUI (Material-UI) の Tabs のデモで紹介されているように React でスワイプを実装するメジャーなライブラリですが、新たなメンテナーが見つからず、2022年10月に非推奨であることが開発者によって明言されました。

Deprecate package, do not use #676
https://github.com/oliviertassinari/react-swipeable-views/issues/676

Swiper

Swiper は元々ピュア JavaScript でスワイプ動作を実装するライブラリでしたが、2020年7月にリリースされた v6 から React に対応し、その後のリリースで VueAngular にも対応しています。また TypeScript 製なので型定義ファイルが含まれています。

Swiper React の基本的な使い方

1
yarn add swiper
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import * as React from 'react';
import { Swiper, SwiperSlide } from 'swiper/react';
import 'swiper/css';

function Page() {
  return (
    <Swiper>
      <SwiperSlide>
        <p>Slide One</p>
      </SwiperSlide>
      <SwiperSlide>
        <p>Slide Two</p>
      </SwiperSlide>
      <SwiperSlide>
        <p>Slide Three</p>
      </SwiperSlide>
    </Swiper>
  );
}

<Swiper><SwiperSlide> の2種類のコンポーネントと swiper/css をインポートすることで Swiper を React で使用することができます。

<Swiper> コンポーネントが Swiper のインスタンス(以降 SwiperCore と表記)を生成し、コンポーネント内部でステートを保持する仕組みになっています。

Swiper React Components
https://swiperjs.com/react

広告

Swiper への移行

ここからは react-swipeable-views が使われている MUI<Tabs> コンポーネントの Full Width の例を Swiper で置き換えていきます。
https://mui.com/material-ui/react-tabs/#full-width

この例では Swiper 単体で完結するコードとは違って、以下の2点を実装する必要があります。

  1. <Tabs> でスライドを制御できるようにする
  2. 同様に、スワイプで操作したスライドと <Tabs> の表示を連動させる

コード例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import * as React from 'react';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
import AppBar from '@mui/material/AppBar';
import Tabs from '@mui/material/Tabs';
import Tab from '@mui/material/Tab';
import type { Swiper as SwiperCore } from 'swiper';
import { Swiper, SwiperSlide } from 'swiper/react';
import 'swiper/css';

type TabPanelProps = React.PropsWithChildren<{
  index: number;
  value: number;
}>;

function TabPanel({ children, value, index }: TabPanelProps) {
  return (
    <div
      role="tabpanel"
      hidden={value !== index}
      id={`full-width-tabpanel-${index}`}
      aria-labelledby={`full-width-tab-${index}`}
    >
      {value === index && (
        <Box sx={{ p: 3 }}>
          <Typography>{children}</Typography>
        </Box>
      )}
    </div>
  );
}

function a11yProps(index: number) {
  return {
    id: `full-width-tab-${index}`,
    'aria-controls': `full-width-tabpanel-${index}`,
  };
}

function FullWidthTabs() {
  const [swiper, setSwiper] = React.useState<SwiperCore | null>(null);
  const [value, setValue] = React.useState(0);

  const handleChange = (event: React.SyntheticEvent, newValue: number) => {
    setValue(newValue);
    swiper?.slideTo(newValue);
  };
  const onSwiper = (currentSwiper: SwiperCore) => {
    const swiperInstance = currentSwiper;
    setSwiper(swiperInstance);
  };
  const onSlideChange = (currentSwiper: SwiperCore) => {
    setValue(currentSwiper.activeIndex);
  };

  return (
    <Box sx={{ bgcolor: 'background.paper', width: 1 }}>
      <AppBar position="static">
        <Tabs
          value={value}
          onChange={handleChange}
          indicatorColor="secondary"
          textColor="inherit"
          variant="fullWidth"
          aria-label="full width tabs example"
        >
          <Tab label="Item One" {...a11yProps(0)} />
          <Tab label="Item Two" {...a11yProps(1)} />
          <Tab label="Item Three" {...a11yProps(2)} />
        </Tabs>
      </AppBar>
      <Swiper
        simulateTouch={false}
        onSwiper={onSwiper}
        onSlideChange={onSlideChange}
      >
        <SwiperSlide>
          <TabPanel value={value} index={0}>
            Item One
          </TabPanel>
        </SwiperSlide>
        <SwiperSlide>
          <TabPanel value={value} index={1}>
            Item Two
          </TabPanel>
        </SwiperSlide>
        <SwiperSlide>
          <TabPanel value={value} index={2}>
            Item Three
          </TabPanel>
        </SwiperSlide>
      </Swiper>
    </Box>
  );
}

デモ (Storybook)
https://cieloazul310.github.io/mui-swiper/?path=/story/swiper--basic

解説

react-swipeable-views では、<SwipeableViews> コンポーネントに props を介してステート value を直接反映できます。したがって、スライドと <Tabs> の表示はどちらも一つのステート value のみで制御することができました。

一方 <Swiper> コンポーネントでは props 経由で表示スライドを制御することができません
Swiperでは <Swiper> コンポーネント内に存在する SwiperCore インスタンスを使って制御する必要があります。ということは、<Tabs> の表示と Swiper の挙動を関連づけるには、SwiperCore インスタンスを <Swiper> コンポーネントより上の階層で利用できるようにしなければなりません。

確認: MUI <Tabs> の基本的な構造

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
function Page() {
  // Tabs のステートフック
  const [value, setValue] = React.useState(0);

  // Tabs のイベントハンドラ
  const handleChange = (event: React.SyntheticEvent, newValue: number) => {
    setValue(newValue);
  };

  // MUI Tabs のコンポーネント (a11yProps は省略)
  return (
    <Tabs
      value={value}
      onChange={handleChange}
    >
      <Tab label="Item One" />
      <Tab label="Item Two" />
      <Tab label="Item Three" />
    </Tabs>
  ); 
}

準備: Swiper のインスタンス SwiperCore をコンポーネントより上の階層で扱う

<Swiper> コンポーネントには SwiperCore インスタンスを受け取る際に発火する onSwiper というイベントハンドラが用意されています。このイベントハンドラを使って SwiperCore インスタンスを <Swiper> コンポーネントより上の階層で利用できるように設定します。

SwiperCore インスタンスを扱うステートフックを作成
1
2
3
import type { Swiper as SwiperCore } from 'swiper';

const [swiper, setSwiper] = React.useState<SwiperCore | null>(null);

ステートフックを作成します。型は SwiperCore | null 、初期値は null とします。

<Swiper> コンポーネントで生成されたインスタンスを定数 swiper にセットする関数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const onSwiper = (currentSwiper: SwiperCore) => {
  const swiperInstance = currentSwiper;
  setSwiper(swiperInstance);
};

return (
  <Swiper onSwiper={onSwiper}>
    <SwiperSlide />
    <SwiperSlide />
    <SwiperSlide />
  </Swiper>
);

<Swiper> コンポーネントの onSwiper イベントハンドラに設定する関数を以上のように定義します。setSwiper<Swiper> コンポーネント内で生成した SwiperCore インスタンスを通すことで、定数 swiper が初期値の null から SwiperCore インスタンスに更新されます。

定義した onSwiper をイベントハンドラに設定することで、<Swiper> コンポーネント内部の SwiperCore インスタンスを swiper という定数で <Swiper> コンポーネントの上の階層で利用できるようになりました。

次に、これを基にして以下の2点を実装していきます。

  1. <Tabs> でスライドを制御できるようにする
  2. 同様に、スワイプで操作したスライドと <Tabs> の表示を連動させる

1. <Tabs> でスライドを制御できるようにする

1
2
3
4
5
const handleChange = (event: React.SyntheticEvent, newValue: number) => {
  setValue(newValue);
  // Swiper を動かす
  swiper?.slideTo(newValue);
};

前述の通り、<Swiper> コンポーネントの外からスライドを動かすには SwiperCore インスタンスを使って制御する必要があります。<Tabs> のイベントハンドラ onChange 内に onSwiper で更新されたステート swiper を使ったコードを加えることで、<Tabs> でスライドを制御できるようになります。

1
2
// 型が SwiperCore | null なのでオプショナルチェーン演算子を付加
swiper?.slideTo(newValue);

SwiperCore のメソッドは以下の API リファレンスで確認できます。

Methods & Properties
https://swiperjs.com/swiper-api#methods-and-properties

2. スワイプで操作したスライドと <Tabs> の表示を連動させる

1
2
3
const onSlideChange = (currentSwiper: SwiperCore) => {
  setValue(currentSwiper.activeIndex);
};

Swiper は Swiper 内部のステートの中で完結する仕様なので、スワイプ操作を行なっても <Tabs> のステート value は更新されません。Swiper と <Tabs> の表示を連動させるには、<Swiper> コンポーネントのイベントハンドラ onSlideChange 内でステート value の値を更新するコードを記述する必要があります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
return (
  <Swiper 
    onSwiper={onSwiper} 
    onSlideChange={onSlideChange}
  >
    <SwiperSlide />
    <SwiperSlide />
    <SwiperSlide />
  </Swiper>
);

これで react-swipeable-views を使った MUI Tabs のコードを Swiper に置き換えることができました。

その他

react-swipeable-views と Swiper のデフォルト値

1
2
3
4
<Swiper simulateTouch={false}>
  <SwiperSlide />
  <SwiperSlide />
</Swiper>

Swiper ではマウスによるスライド操作がデフォルトで有効になっています。react-swipeable-views ではデフォルトで無効になっているため、動作を合わせるには <Swiper> コンポーネントの simulateTouch props を false に設定します。逆に react-swipeable-views でマウスによるスライド操作を有効にするには <SwipeableViews> コンポーネントに enableMouseEvents props を渡します。

Swiper のモジュール

Swiper では Navigation や Pagination など様々なモジュールが用意されています。React で利用するには <Swiper> コンポーネントの modules props にインポートしたモジュールを配列として指定することで実装できます。

Demos
https://swiperjs.com/demos

以下のコードはキーボードによるスライド操作を実装する Keyboard モジュールを実装する例です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import * as React from 'react';
import { type Swiper as SwiperCore, Keyboard } from 'swiper';
import { Swiper, SwiperSlide } from 'swiper/react';
import 'swiper/css';

function Page() {
  return (
    <Swiper
      modules={[Keyboard]}
      keyboard={{
        enabled: true,
      }}
    >
      <SwiperSlide />
      <SwiperSlide />
      <SwiperSlide />
    </Swiper>
  );
}

Using JS Modules
https://swiperjs.com/swiper-api#using-js-modules

Storybook による動作デモ

Storybook で作成したデモページとリポジトリです

動作デモ
https://cieloazul310.github.io/mui-swiper/

リポジトリ
https://github.com/cieloazul310/mui-swiper

リンク

Bad Moon Rising / Creedence Clearwater Revival (1969)

広告