2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

はじめに

概要

この記事は、トラハック氏(@torahack_)が運営するYoutubeチャンネル『トラハックのエンジニア学習ゼミ【とらゼミ】』の『日本一わかりやすいReact-Redux講座 実践編』の学習備忘録です。

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

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

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

動画URL

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

要点

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

完成系イメージ

http://localhost:3000/order/confirm

image.png

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

http://localhost:3000/order/completes

image.png

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

image.png

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

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

image.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
1.src/reducks/store/initialState.js
const initialState = {
  products: {
    list: []
  },

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

export default initialState

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

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()を定義します。

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
  }
}
.
.
.
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
  }
}
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

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

実装ファイル(コンポーネント)
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
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;
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として取得します。
  • ordersmapを用いてイテレートし、<OrderHistoryItem>へ展開します。
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>に渡します。
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;

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

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"

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

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)のフォローもよろしくお願いします。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?