【備忘録】日本一わかりやすいReact-Redux講座 実践編 #13 「注文履歴を確認しよう〜コンポーネントの再利用〜」

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

前回の講座で、トランザクション処理を含めた購入処理を実装しました。

今回は、これまでに作成してきたコンポーネントを再利用しながら、注文履歴画面を作成します。

※ 前回記事: [【備忘録】日本一わかりやすいReact-Redux講座 実践編 #12後半 「トランザクションを使って商品を注文しよう（後半）」](https://qiita.com/ddpmntcpbr/items/a08d67410e82e22f77f1)


### 動画URL
[注文履歴を確認しよう〜コンポーネントの再利用〜【日本一わかりやすいReact-Redux講座 実践編#13】](https://www.youtube.com/watch?v=mHz8Ll8xtjE)

### 要点
- React コンポーネントを再利用することで、新規コンポーネントの作成が効率化される。

### 完成系イメージ

**``http://localhost:3000/order/confirm``**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540216/17f5c919-1242-0b51-3e0a-727889103ec2.png)

カート内の商品を入れた状態で、「注文を確定する」をクリックすると、

**``http://localhost:3000/order/completes``**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540216/9b01935d-fc04-6323-b3ce-0aa23fb2f226.png)

ここは未定義です。Drawerメニューの「商品履歴」をクリックすると、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540216/d7ef54be-69c4-b80f-54b3-dc28325f8215.png)

カートに入れた商品が、購入履歴として表示されています。

再度、商品を購入すると、別の注文IDとして購入履歴が追加されていきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540216/02980c7d-57ba-8306-667d-237cd45e5e4d.png)


## メイン

### 実装概要

今回は、購入履歴を確認する画面を作ります。これまでの講座で学習してきたことの復習になります。

前回の講座で、DBの``users``コレクションのサブコレクションとして、``orders``サブコレクションを定義しました。

Redux Storeについても、同様のデータ構造で購入履歴を設計します。具体的には、

- ``initialState.js``内で、``users``の配列要素として``orders``配列を定義
- ``orders``を　Store から取得して React で使用するための operations, actions, reducers., selectors を定義

を実装します。

また、購入履歴用のテンプレートを新たに作ります。

- templates: ``OrderHistory.jsx``
- route: ``/order/history``

とします。

### reducksファイル実装

```:実装ファイル(reducks)
1.src/reducks/store/initialState.js
2.src/reducks/users/operations.js
3.src/reducks/users/actions.js
4.src/reducks/users/reducers.js
5.src/reducks/users/selectors.js
```

```js:1.src/reducks/store/initialState.js
const initialState = {
  products: {
    list: []
  },

  users: {
    cart: [],
    isSignedIn: false,
    orders: [], //追記
    role:  "",
    uid: "",
    username: ""
  }
};

export default initialState

```

``users``の配列要素として、``orders``配列を定義します。


```js:2.src/reducks/users/operations.js
import { fetchOrdersHistoryAction, fetchProductsInCartAction,signInAction,signOutAction } from "./actions"; //追記
import { push } from "connected-react-router";
import {auth, db, FirebaseTimestamp} from "../../firebase/index"
.
.
.
//追記
export const fetchOrdersHistory = () => {
  return async (dispatch, getState) => {
    const uid = getState().users.uid;
    const list = []

    db.collection("users").doc(uid).collection('orders')
      .orderBy('updated_at', "desc")
      .get()
      .then(snapshots => {
        snapshots.forEach(snapshot => {
          const data = snapshot.data();
          list.push(data)
        });
        dispatch(fetchOrdersHistoryAction(list))
      })
  }
}
//追記ここまで
.
.
.
```

DB上の``orders``を取得し、オブジェクト配列としてアクションに渡す``fetchOrdersHistory()``を定義します。

```js:3.src/reducks/users/actions.js
export const FETCH_ORDERS_HISTORY = "FETCH_ORDERS_HISTORY";
export const fetchOrdersHistoryAction = (orders) => {
  return {
    type: "FETCH_ORDERS_HISTORY",
    payload: orders
  }
}
.
.
.
```

```js:4.src/reducks/users/reducers.js
import * as Actions from './actions'
import initialState from '../store/initialState'

export const UsersReducer = (state = initialState.users, action) => {
  switch (action.type) {
    //追記
    case Actions.FETCH_ORDERS_HISTORY:
      return {
        ...state,
        orders: [...action.payload]
      };
    //追記ここまで
    case Actions.FETCH_PRODUCTS_IN_CART:
      return {
        ...state,
        cart: [...action.payload]
      };
    case Actions.SIGN_IN:
      return {
        ...state,
        ...action.payload
      };
    case Actions.SIGN_OUT:
      return {
        ...action.payload
      };
    default:
      return state
  }
}
```

```js:5.src/reducks/users/selectors.js
import { createSelector } from "reselect";

const usersSelector = (state) => state.users;
.
.
.
export const getOrdersHistory = createSelector(
  [usersSelector],
  state => state.orders
)
.
.
.
```

actions, reducers,selectorsを定義します。

``getOrdersHistory()``から、Store内の``orders``を参照できるようになりました。

### コンポーネントファイル実装

コンポーネント構想は以下のイメージ。

![13-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540216/7f544ff5-7a3e-925b-070f-468080a9ba6c.png)

ここに書いてあるコンポーネントだけでなく、**過去の講座で作成したコンポーネントをどんどん再利用していきます**。



```:実装ファイル(コンポーネント)
1.src/templates/OrderHistory.jsx
2.src/templates/index.js
3.src/components/Products/OrderHistoryItem.jsx
4.src/components/Products/OrderedProducts.jsx
5.src/components/Products/index.js
6.src/Router.jsx
```

```jsx:1.src/templates/OrderHistory.jsx
import React, {useEffect} from 'react';
import {useDispatch, useSelector} from "react-redux";
import List from "@material-ui/core/List";
import {getOrdersHistory} from "../reducks/users/selectors";
import {OrderHistoryItem} from "../components/Products";
import {fetchOrdersHistory} from "../reducks/users/operations";
import {makeStyles} from "@material-ui/styles";

const useStyles = makeStyles((theme) => ({
  orderList: {
    background: theme.palette.grey["100"],
    margin: '0 auto',
    padding: 32,
    [theme.breakpoints.down('sm')]: {
      width: '100%'
    },
    [theme.breakpoints.up('md')]: {
      width: 768
    }
  },
}))

const OrderHistory = () => {
  const classes = useStyles()
  const dispatch = useDispatch()
  const selector = useSelector(state  => state)
  const orders = getOrdersHistory(selector);

  useEffect(() => {
      dispatch(fetchOrdersHistory())
  },[])

  return (
    <section className="c-section-wrapin">
      <List className={classes.orderList}>
        {orders.length > 0 && (
          orders.map(order => <OrderHistoryItem order={order} key={order.id} />)
        )}
      </List>
    </section>
  );
};

export default OrderHistory;
```

```js:2.src/templates/index.js
export {default as CartList} from './CartList'
export {default as Home} from './Home'
export {default as OrderConfirm} from './OrderConfirm'
export {default as OrderHistory} from './OrderHistory' //追記
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'
```

- ``useEffect()``内で``fetchOrdersHistory()``を実行し、DB上の``orders``サブコレクションを取得してStoreに保存します。
- ``const orders = getOrdersHistory(selector);``で、Storeに保存した``orders``をstateとして取得します。
- ``orders``を``map``を用いてイテレートし、``<OrderHistoryItem>``へ展開します。

```jsx:3.src/components/Products/OrderHistoryItem.jsx
import React from 'react';
import Divider from "@material-ui/core/Divider";
import {TextDetail} from "../UIkit";
import {OrderedProducts} from "./index";

const datetimeToString = (date) => {
  return  date.getFullYear() + "-"
          + ("00" + (date.getMonth()+1)).slice(-2) + "-"
          + ("00" + date.getDate()).slice(-2) + " "
          + ("00" + date.getHours()).slice(-2) + ":"
          + ("00" + date.getMinutes()).slice(-2) + ":"
          + ("00" + date.getSeconds()).slice(-2)
}

const dateToString = (date) => {
  return  date.getFullYear() + "-"
          + ("00" + (date.getMonth()+1)).slice(-2) + "-"
          + ("00" + date.getDate()).slice(-2)
}

const OrderHistoryItem = (props) => {
  const order = props.order;
  const orderedDatetime = datetimeToString(props.order.updated_at.toDate())
  const price = "¥" + order.amount.toLocaleString()
  const shippingDate = dateToString(props.order.shipping_date.toDate())
  const products = props.order.products

  return (
    <div>
      <div className="module-spacer--small" />
      <TextDetail label={"注文ID"} value={order.id} />
      <TextDetail label={"注文日時"} value={orderedDatetime} />
      <TextDetail label={"発送予定日"} value={shippingDate} />
      <TextDetail label={"注文金額"} value={price}/>
      {products.length > 0 && (
        <OrderedProducts products={products} />
      )}
      <div className="module-spacer--extra-extra-small" />
      <Divider />
    </div>
  );
};

export default OrderHistoryItem;
```

- ``<TextDetail>``コンポーネントを再利用しています。
- ``props.order``が持っている``products``を``<OrderedProducts>``に渡します。

```jsx:4.src/components/Products/OrderedProducts.jsx
import React, {useCallback} from 'react';
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
import ListItemAvatar from "@material-ui/core/ListItemAvatar";
import ListItemText from "@material-ui/core/ListItemText";
import Divider from "@material-ui/core/Divider";
import {makeStyles} from "@material-ui/styles";
import {PrimaryButton} from "../UIkit";
import {useDispatch} from "react-redux";
import {push} from "connected-react-router"

const useStyles = makeStyles((theme) => ({
  list: {
    background: '#fff',
    height: 'auto'
  },
  image: {
    objectFit: 'cover',
    margin: '8px 16px 8px 0',
    height: 96,
    width: 96
  },
  text: {
    width: '100%'
  }
}))

const OrderedProducts = (props) => {
  const classes = useStyles();
  const dispatch = useDispatch();
  const products = props.products;

  const goToProductDetail = useCallback((id) => {
    dispatch(push('/product/'+id))
  }, [])

  return (
    <List>
      {products.map(product => (
        <>
          <ListItem className={classes.list} key={product.id}>
            <ListItemAvatar>
              <img
                className={classes.image}
                src={product.images[0].path}
                alt="商品のTOP画像"
              />
            </ListItemAvatar>
            <div className={classes.text}>
              <ListItemText
                primary={product.name}
                secondary={"サイズ：" + product.size}
              />
              <ListItemText
                primary={"¥"+product.price.toLocaleString()}
              />
            </div>
            <PrimaryButton label={"商品詳細を見る"} onClick={() => goToProductDetail(product.id)} />
          </ListItem>
          <Divider />
        </>
      ))}
    </List>
  );
}

export default OrderedProducts;
```

親コンポーネントから渡ってきた``products``を``map``でイテレートして、リスト表示しています。

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

エントリーポイントに追加します。

```jsx:6.src/Router.jsx
import React from 'react';
import {Route, Switch} from "react-router";
import {CartList, OrderConfirm,OrderHistory,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} />
        <Route extct path={"/cart"} component={CartList} />
        <Route extct path={"/order/confirm"} component={OrderConfirm} /> {*追記*}
        <Route extct path={"/order/history"} component={OrderHistory} /> {*追記*}
      </Auth>
    </Switch>
  );
};

export default Router
```

ルーティングを定義します。


以上で実装は完了です！


## さいごに
今回の要点をおさらいすると、

- React コンポーネントを再利用することで、新規コンポーネントの作成が効率化される。

以上です！

**開発を進めれば進めるほどコンポーネントが豊富になり、より開発速度が上がる**というのが。Reactの素晴らしいところですね。

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