はじめに
概要
この記事は、トラハック氏(@torahack_)が運営するYoutubeチャンネル『トラハックのエンジニア学習ゼミ【とらゼミ】』の『日本一わかりやすいReact-Redux講座 実践編』の学習備忘録です。
前回の講座で、トランザクション処理を含めた購入処理を実装しました。
今回は、これまでに作成してきたコンポーネントを再利用しながら、注文履歴画面を作成します。
※ 前回記事: 【備忘録】日本一わかりやすいReact-Redux講座 実践編 #12後半 「トランザクションを使って商品を注文しよう(後半)」
動画URL
注文履歴を確認しよう〜コンポーネントの再利用〜【日本一わかりやすいReact-Redux講座 実践編#13】
要点
- React コンポーネントを再利用することで、新規コンポーネントの作成が効率化される。
完成系イメージ
http://localhost:3000/order/confirm
カート内の商品を入れた状態で、「注文を確定する」をクリックすると、
http://localhost:3000/order/completes
ここは未定義です。Drawerメニューの「商品履歴」をクリックすると、
カートに入れた商品が、購入履歴として表示されています。
再度、商品を購入すると、別の注文IDとして購入履歴が追加されていきます。
メイン
実装概要
今回は、購入履歴を確認する画面を作ります。これまでの講座で学習してきたことの復習になります。
前回の講座で、DBのusers
コレクションのサブコレクションとして、orders
サブコレクションを定義しました。
Redux Storeについても、同様のデータ構造で購入履歴を設計します。具体的には、
-
initialState.js
内で、users
の配列要素としてorders
配列を定義 -
orders
を Store から取得して React で使用するための operations, actions, reducers., selectors を定義
を実装します。
また、購入履歴用のテンプレートを新たに作ります。
- templates:
OrderHistory.jsx
- route:
/order/history
とします。
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
const initialState = {
products: {
list: []
},
users: {
cart: [],
isSignedIn: false,
orders: [], //追記
role: "",
uid: "",
username: ""
}
};
export default initialState
users
の配列要素として、orders
配列を定義します。
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()
を定義します。
export const FETCH_ORDERS_HISTORY = "FETCH_ORDERS_HISTORY";
export const fetchOrdersHistoryAction = (orders) => {
return {
type: "FETCH_ORDERS_HISTORY",
payload: orders
}
}
.
.
.
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
}
}
import { createSelector } from "reselect";
const usersSelector = (state) => state.users;
.
.
.
export const getOrdersHistory = createSelector(
[usersSelector],
state => state.orders
)
.
.
.
actions, reducers,selectorsを定義します。
getOrdersHistory()
から、Store内のorders
を参照できるようになりました。
コンポーネントファイル実装
コンポーネント構想は以下のイメージ。
ここに書いてあるコンポーネントだけでなく、過去の講座で作成したコンポーネントをどんどん再利用していきます。
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
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;
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>
へ展開します。
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>
に渡します。
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
でイテレートして、リスト表示しています。
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"
エントリーポイントに追加します。
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)のフォローもよろしくお願いします。