【備忘録】日本一わかりやすいReact-Redux講座 実践編 #8 「Swiperで画像スライダーを作ろう」

## はじめに
この記事は、トラハック氏([@torahack_](https://twitter.com/torahack_))が運営するYoutubeチャンネル『トラハックのエンジニア学習ゼミ【とらゼミ】』の『[日本一わかりやすいReact-Redux講座 実践編](https://www.youtube.com/watch?v=8fNgXnVTULU)』の学習備忘録です。

前回まで講座で、Products の CRUD 機能を一通り実装しました。

今回の講座では、
- ``React-swiper``を用いた商品情報詳細ページ(CRUD の R)
- ``MuiThemeProvider``による Material-UI のテーマカラー設定

について実装します。

※ 前回記事: [【備忘録】日本一わかりやすいReact-Redux講座 実践編 #7 「商品を一覧表示しよう」](https://qiita.com/ddpmntcpbr/items/e93afdb0b01a6c9297e8)

## 要点
- ``<MuiThemeProvider>``で、Material-UI のテーマカラーを設定できる。
- ``react-swiper``で画像スワイプ機能を実装できる。
- Material-UIの``<Table>``コンポーネントでテーブルを作成できる。

## 完成系イメージ

**``http://localhost:3000/product/jJV3RsxCgFllZeo771wR``**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540216/5a8ff6f7-eb19-6fc0-79a3-fea8c5f52c72.png)

商品情報の詳細ページを作成します。

左側が画像スライダー、右側がサイズ表示用のテーブルになっています。

サイズ表示用テーブルは、Material-UIの``<Table>``コンポーネントを使うことで比較的容易に作成できます。

画像スライダーは、``react-id-swiper``というnpmパッケージをインストールすることで、非常に簡単に実装することができます。

## メイン
### MuiThemeProvider によるテーマカラー設定

Material-UI において、アプリ内で使用するテーマカラーを設定する機能として``MuiThemeProvider``というものがあります。

これを用いることで、例えばコンポーネント内のフォント一つ一つにカラーコードを定義するような必要がなくなります。

また、「primaryが赤、secondaryが青」のように限られた数の色をテーマカラーを設定して運用するため、無駄な色の使いすぎを自然と防ぐことにも繋がります。

今回は既存のthemeファイルを読み込むだけですが、一連の流れが理解できれば、テーマカラーを簡単に変更することができます。

```:実装ファイル(theme関連)
1.src/assets/theme.js  ## themeカラーが定義されたファイル
2.src/index.js  ## theme.jsを読み込み、アプリ全体に適用
```

```javascript:src/assets/theme.js
import { createMuiTheme } from '@material-ui/core/styles';

// Pick colors on https://material.io/resources/color/#!/

export const theme = createMuiTheme({
  palette: {
    primary: {
      light: '#88ffff',
      main: '#4dd0e1',
      dark: '#009faf',
      contrastText: '#000',
    },
    secondary: {
      light: '#ffff81',
      main: '#ffd54f',
      dark: '#c8a415',
      contrastText: '#000',
    },
    grey: {
      50: "#fafafa",
      100: "#f5f5f5",
      200: "#eeeeee",
      300: "#e0e0e0",
      400: "#bdbdbd",
      500: "#9e9e9e",
      600: "#757575",
      700: "#616161",
      800: "#424242",
      900: "#212121",
      A100: "#d5d5d5",
      A200: "#aaaaaa",
      A400: "#303030",
      A700: "#616161"
    }
  },
});
```

themeカラーをカラーコードとして定義します。これを、アプリ全体に適用させます。

```javascript:src/index.js
import React from 'react';
.
.
.
import {MuiThemeProvider} from "@material-ui/core";
import {theme} from "./assets/theme"
.
.
.
ReactDOM.render(
  <Provider store={store}>
    <ConnectedRouter history={history}>
      <MuiThemeProvider theme={theme}>
        <App />
      </MuiThemeProvider>
    </ConnectedRouter>
  </Provider>,
  document.getElementById('root')
);
.
.
.
```

``<MuiThemeProvider>``で``<App />``を丸ごとラッピングして、設定完了です。

これにより、各コンポーネントの``makeStyle()``内において、

```:任意のコンポーネントファイル
const useStyles = makeStyles((theme) => ({
  root: {
    color: theme.palette.primary.main
  }
  .
  .
  .
}));
```

``theme.js``で定義した色を適用することができます。

今回の``theme.js``では、primaryが水色系、secondaryが黄色系で配色されています。

### 詳細ページの大枠作成

商品情報詳細ページの大枠を作成します。

ひとまずは、URLに含まれるid情報を元に正しいproductsをCloud Firstoreから取り出すところまで進めます。テンプレート、及びパスは、

- テンプレート: ``ProductDetail.jsx``
- path: ``/product/:id``

として実装します。

```:実装ファイル(詳細ページ大枠)
1.src/Router.jsx
2.src/templates/ProductDetail.jsx
3.src/templates/index.js
```

```jsx:1.src/Router.jsx
import React from 'react';
import {Route, Switch} from "react-router";
import {ProductDetail,ProductEdit,ProductList,Reset,SignIn,SignUp} from "./templates";
import Auth from "./Auth"

const Router = () => {
  return (
    <Switch>
      <Route exact path={"/signup"} component={SignUp} />
      <Route exact path={"/signin"} component={SignIn} />
      <Route exact path={"/signin/reset"} component={Reset} />

      <Auth>
        <Route exact path={"(/)?"} component={ProductList} />
        <Route exact path={"/product/:id"} component={ProductDetail} /> {/*追記*/}
        <Route path={"/product/edit(/:id)?"} component={ProductEdit} />
      </Auth>
    </Switch>
  );
};

export default Router
```

```jsx:2.src/templates/ProductDetail.jsx
import React,{ useState,useEffect,useCallback } from "react";
import {useSelector} from "react-redux"
import {db} from "../firebase"
import { makeStyles } from "@material-ui/core";
import HTMLReactParser from "html-react-parser"

const useStyles = makeStyles((theme)=>({
  sliderBox: {
    [theme.breakpoints.down("sm")]: {
      margin: "0 auto 24px auto",
      height: 320,
      width: 320
    },
    [theme.breakpoints.up("sm")]:{
      margin: "0 auto",
      height: 400,
      width: 400
    }
  },
  detail: {
    [theme.breakpoints.down("sm")]: {
      margin: "0 auto 16px auto",
      height: "auto",
      width: 320
    },
    [theme.breakpoints.up("sm")]:{
      margin: "0 auto",
      height: "auth",
      width: 400
    }
  },
  price: {
    fontSize: 36
  }
}));

const returnCodeToBr = (text) => {
  if (text === "") {
      return text
  } else {
      return HTMLReactParser(text.replace(/\r?\n/g, '<br/>'))
  }
};

const ProductDetail = () => {
  const classes = useStyles();
  const selector = useSelector((state)=>state);
  const path = selector.router.location.pathname;
  const id = path.split("/product/")[1];

  const [product,setProduct] = useState();

  useEffect(()=>{
    db.collection("products").doc(id).get()
    .then(doc => {
      const data = doc.data();
      setProduct(data)
    })
  },[]);

  return (
    <section className="c-sention-wrapin">
      {product && (
        <div className="p-grid__row" >
          <div className={classes.sliderBox}>

          </div>
          <div className={classes.detail}>
            <h2 className="u-text__headline">{product.name}</h2>
            <p className={classes.price}>{product.price.toLocaleString()}</p>
            <div className="module-spacer--small"></div>
            <div className="module-spacer--small"></div>
            <p>{returnCodeToBr(product.description)}</p>
          </div>
        </div>
      )}
    </section>
  )
};

export default ProductDetail
```

- ``[theme.breakpoints.down("sm")]: {...}``とすることで、smサイズの幅をブレイクポイントとして、適用するスタイルを変更しています。
- ``useEffect()``を用いて、初期レンダー時に、DBから該当するidの情報を取得します。
- ``returnCodeToBr()``で、改行コード``\n``を、改行のHTMLタグ``<br />``に変換しています。この関数に用いている``html-react-parser``は、この時点ではインストールしていないはずなので、npm からインストールします。


```terminal:html-react-parserのインストール
$ npm install --save html-react-parser
```

```javascript:3.src/templates/index.js
export {default as Home} from './Home'
export {default as ProductDetail} from './ProductDetail'
export {default as ProductEdit} from './ProductEdit'
export {default as ProductList} from './ProductList'
export {default as Reset} from './Reset'
export {default as SignIn} from './SignIn'
export {default as SignUp} from './SignUp'
```

いったん動作確認をします。すでに``ProductCard.jsx``において、リストアイテムをクリックすることで``/product/:id``へ遷移されるよう定義されているため、そこからアクセスします。

**``http://localhost:3000``**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540216/d5376718-d77a-6f82-6063-ac2d9fe7e6f8.png)

スカートをクリックすると、

**``http://localhost:3000/product/ewzuLXPK2Dr0owCG3dt7``**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540216/25d7dfd5-64c6-08ae-5639-ac3e78eb9b33.png)

スカートの商品情報が取得できていることが分かります！

### 画像スライダーの設置

この``ProductDetail``テンプレートに対して、画像スライダーを設置します。

画像スライダーの実装には、``react-id-swiper``というnpmパッケージを利用します。依存関係にある``swiper``と合わせて、インストールしておきます。

```terminal:swiperとreact-id-swiperをインストール
$  npm install -S swiper@5.4.2 react-id-swiper@3.0.0  
```

画像スライダー用のコンポーネントして``ImageSwiper.jsx``を定義し、それを``ProductDetail.jsx``で読み込みます。

```:実装ファイル（画像スライダー）
1.src/components/Products/ImageSwiper.jsx
2.src/components/Products/index.js
3.src/templates/ProductDetail.jsx
```

```jsx:1.src/components/Products/ImageSwiper.jsx
import React, { useState } from "react";
import Swiper from "react-id-swiper";
import NoImage from "../../assets/img/src/no_image.png"
import 'swiper/css/swiper.css'

const ImageSwiper = (props) => {
  const [params] = useState({
    pagination: {
      el: ".swiper-pagination",
      type: "bullets",
      clickable: true,
      dynamicBullets: true
    },
    navigation: {
      nextEl: ".swiper-button-next",
      prevEl: ".swiper-button-prev"
    },
    loop: true
  })

  const images = props.images

  return (
    <Swiper {...params}>
      {images.length === 0 ? (
        <div className="p-media__thumb">
          <img src={NoImage} alt="no images"/>
        </div>
      ) : (
        images.map(image => (
          <div className="p-media__thumb" key={image.id}>
            <img src={image.path} alt="商品情報"/>
          </div>
        ))
      )}
    </Swiper>
  );
};

export default ImageSwiper;
```

``react-id-swiper``は、

1.``import 'swiper/css/swiper.css'``でスライダー用のcssを読み込む。
1. スライダーの性質を``params``として定義
2. ``<Swiper {...params}>``としてJSXに展開

という流れで実装をします。

``'swiper/css/swiper.css'``は、npmインストールすることによって、``node_modules``下に保存されるcssファイルです。この中で、スライダーとしての動的な挙動が実装されているイメージです。

また、``params``の中で定義するパラメータを変えることで、様々な形式のスライダーを実装できるようです([公式リファレンス参照](https://www.npmjs.com/package/react-id-swiper))



```javascript::2.src/components/Products/index.js
export {default as ImageArea} from "./ImageArea"
export {default as ImagePreview} from "./ImagePreview"
export {default as ImageSwiper} from "./ImageSwiper" //追記
export {default as ProductCard} from "./ProductCard"
export {default as SetSizeArea} from "./SetSizeArea"
```


```jsx:3.src/templates/ProductDetail.jsx
.
.
.
import {ImageSwiper} from "../components/Products" {*追記*}
.
.
.
const ProductDetail = () => {
  .
  .
  .
  return (
    <section className="c-sention-wrapin">
      {product && (
        <div className="p-grid__row" >
          <div className={classes.sliderBox}>
            <ImageSwiper images={product.images} /> {*追記*}
          </div>
          <div className={classes.detail}>
            <h2 className="u-text__headline">{product.name}</h2>
            <p className={classes.price}>{product.price.toLocaleString()}</p>
            <div className="module-spacer--small"></div>
            <div className="module-spacer--small"></div>
            <p>{returnCodeToBr(product.description)}</p>
          </div>
        </div>
      )}
    </section>
  )
};

export default ProductDetail
```


#### 画像スライダー動作確認

**``http://localhost:3000/product/jJV3RsxCgFllZeo771wR``**

![8-1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540216/69ca1f45-4196-aaaf-2e42-abf733ebe6a2.gif)

コード量も少なく簡単に画像スライダーが実装できるのはすばらしいですね！

###  サイズ表示テーブル

最後に、商品情報のサイズを表示するテーブルを作成します。

``<SizeTable>``コンポーネントを作成し、``<ProductDetail>``で読み込みます。

```:実装ファイル（サイズ表示テーブル）
1.src/components/Products/SizeTable.jsx
2.src/components/Products/index.js
3.src/templates/ProductDetail.jsx
```

```jsx:1.src/components/Products/SizeTable.jsx
import React from 'react';
import Table from "@material-ui/core/Table";
import TableRow from "@material-ui/core/TableRow";
import TableCell from "@material-ui/core/TableCell";
import TableBody from "@material-ui/core/TableBody";
import IconButton from "@material-ui/core/IconButton";
import TableContainer from "@material-ui/core/TableContainer";
import ShoppingCartIcon from '@material-ui/icons/ShoppingCart';
import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder';
import {makeStyles} from "@material-ui/styles";

const useStyles = makeStyles({
  iconCell: {
    padding: 0,
    height: 48,
    width: 48
  }
})

const SizeTable = (props) => {
  const classes = useStyles();

  const sizes = props.sizes;

  return (
    <TableContainer>
      <Table>
        <TableBody>
          {sizes.length > 0 && (
            sizes.map(size => (
              <TableRow key={size.size}>
                <TableCell component="th" scope="row">{size.size}</TableCell>
                <TableCell>残り{size.quantity}点</TableCell>
                <TableCell className={classes.iconCell}>
                  {size.quantity > 0 ? (
                    <IconButton>
                      <ShoppingCartIcon />
                    </IconButton>
                  ) : (
                    <div>売切</div>
                  )}
                </TableCell>
                <TableCell className={classes.iconCell}>
                  <IconButton>
                    <FavoriteBorderIcon />
                  </IconButton>
                </TableCell>
            </TableRow>
            ))
          )}
        </TableBody>
      </Table>
    </TableContainer>
  );
};

export default SizeTable;
```

Material-UIの

- ``<TableContainer>``
- ``<Table>``
- ``<TableBody>``
- ``<TableRow>``
- ``<TableCell>``

で、テーブルを実装しています。

```js:2.src/components/Products/index.js
export {default as ImageArea} from "./ImageArea"
export {default as ImagePreview} from "./ImagePreview"
export {default as ImageSwiper} from "./ImageSwiper"
export {default as ProductCard} from "./ProductCard"
export {default as SetSizeArea} from "./SetSizeArea"
export {default as SizeTable} from "./SizeTable" //追記
```

```jsx:3.src/templates/ProductDetail.jsx
.
.
.
import {SizeTable} from "../components/Products" {*追記*}
.
.
.
const ProductDetail = () => {
  .
  .
  .
  return (
    <section className="c-sention-wrapin">
      {product && (
        <div className="p-grid__row" >
          <div className={classes.sliderBox}>
            <ImageSwiper images={product.images} />
          </div>
          <div className={classes.detail}>
            <h2 className="u-text__headline">{product.name}</h2>
            <p className={classes.price}>{product.price.toLocaleString()}</p>
            <div className="module-spacer--small"></div>
            <SizeTable sizes={product.sizes}/>  {*追記*}
            <div className="module-spacer--small"></div>
            <p>{returnCodeToBr(product.description)}</p>
          </div>
        </div>
      )}
    </section>
  )
};

export default ProductDetail
```

``<SizeTable>``コンポーネントをインポートします。

###　動作確認

**``http://localhost:3000/product/jJV3RsxCgFllZeo771wR``**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540216/5315ce5f-a215-d97b-6d76-828e815da6f6.png)

左に画像スライダー、右にサイズテーブルが設置されています！

## さいごに
今回の要点をおさらいすると、
- ``<MuiThemeProvider>``で、Material-UI のテーマカラーを設定できる。
- ``react-swiper``で画像スワイプ機能を実装できる。
- Material-UIの``<Table>``コンポーネントでテーブルを作成できる。

以上です！

このような学習内容を日々呟いていますので、よろしければTwitter([@ddpmntcpbr](https://twitter.com/ddpmntcpbr))のフォローもよろしくお願いします。