Edited at

Reduxのサンプルのショッピングカートをステップ・バイ・ステップで Part.3

More than 1 year has passed since last update.

Reduxのサンプルのショッピングカートをステップ・バイ・ステップで Part.2のつづき。


1.カートに入れるアクション追加


アクションのtypeを追加


constants\ActionTypes.js

export const RECEIVE_PRODUCTS = 'RECEIVE_PRODUCTS'

export const ADD_TO_CART = 'ADD_TO_CART'


ADD_TO_CARTアクションを発行する

ADD_TO_CARTアクションを発行するaddToCart関数をつくる。


actions\index.js

import shop from '../api/shop'

import * as types from '../constants/ActionTypes'

//引数1個を受ける関数を返す関数。 この引数には実行時、dispatchが割り当てられる。
export const getAllProducts = () => d => {
// コールバックが引数。実行時、商品データの配列が入る。api/products.json
shop.getProducts(
products => d(
{
type: types.RECEIVE_PRODUCTS,
products
}
)
)
}
//引数2個を受ける関数を返す関数。
// この引数には実行時、dとgにはdispatchとgetStateが割り当てられる。
export const displayState = () => (d, g) => {
console.log(g());
}
//実行時、dとgにはdispatchとgetStateが割り当てられる。
//g すなわち getState() は state tree を返す。
export const addToCart = (productId) => (d, g) => {
if (g().products.byId[productId].inventory > 0) {
d({
type: types.ADD_TO_CART,
productId
})
}
}



productsリデューサをADD_TO_CARTに対応させる

変更したところは、byIdリデューサにADD_TO_CARTアクションのときの対応を追加。

"カートに入れる"ボタンを押された商品のinventroyを1減じて、もとのbyIdステートにマージ。

products関数という補助的な関数も追加して、byIdの内部で利用できるようにしている。


reducers\products.js

import { combineReducers } from 'redux';

import { RECEIVE_PRODUCTS, ADD_TO_CART } from '../constants/ActionTypes'

//stateは特定の商品のオブジェクトを想定
// ADD_TO_CARTのときは、引数のオブジェクトのinventoryプロパティの値を
// 1 減じたオブジェクトにして返す。
const products = (state, action) => {

switch (action.type) {
case ADD_TO_CART:
return {
...state,
inventory: state.inventory - 1
}

default:
return state;
}

}

// stateはbyId
const byId = (state = {}, action) => {
switch (action.type) {
case RECEIVE_PRODUCTS:
return {
...state,
...action.products.reduce(
(obj, product) => {
obj[product.id] = product;
return obj;
}
, {}
)
}
// ADD_TO_CARTはこちら
default:
const { productId } = action;
return {
...state,
[productId]: products(state[productId], action)
}
}
}
// stateはvisibleIds
const visibleIds = (state = [], action) => {
switch (action.type) {
case RECEIVE_PRODUCTS:
return action.products.map(product => product.id);
default:
return state
}
}

export default combineReducers({ byId, visibleIds })

//idでstateから特定の商品オブジェクトを取得
export const getProduct = (state, id) => {
return state.byId[id]
}
//商品idの配列から、商品オブジェクトの配列取得
export const getVisibleProducts = state => {
return state.visibleIds.map(id => getProduct(state, id))
}



ProductItemコンポーネントのクリックを有効化

buttonのonClickのイベントハンドルに onAddToCartClicked を指定。これの中身は

ProductsContainerコンテナでわたされる、addToCart(product.id)の実行を命令する関数。


components\ProductItem.js

import React from 'react'

import PropTypes from 'prop-types'
import Product from './Product'

//個々の商品データを入れる入れ物
// カートに入れるためのボタンもつける
const ProductItem = ({product,onAddToCartClicked})=>(
<div className="clearfix p-1 rounded mx-auto" style={{ marginBottom: 5, width: '30em', border:'5px solid #eee'}} >
<Product
title={product.title}
price={product.price}
quantity={product.inventory} />
<hr />
<button className="btn btn-warning float-right"
onClick={onAddToCartClicked}
disabled={product.inventory > 0 ? '' : 'disabled'}>
{product.inventory > 0 ? 'カートに入れる' : '売り切れです'}
</button>
</div>
)

ProductItem.propTypes = {
product: PropTypes.shape({
title: PropTypes.string.isRequired,
price: PropTypes.number.isRequired,
inventory: PropTypes.number.isRequired
}).isRequired,
onAddToCartClicked:PropTypes.func.isRequired
}

export default ProductItem



ProductsContainerコンテナ修正

ProductsContainerコンテナは、addToCart関数をプロップスとして受け取り、これを一つ一つのProductItemコンポーネントのonAddToCartClickedプロップスに

() => addToCart(product.id)

という関数を指定して渡している。

connect関数の第2引数のオブジェクトにaddToCartを追加するのを忘れないよう注意。


containers\ProductsContainer.js

const ProductsContainer = ({ products, displayState, addToCart }) => (

<ProductsList title="商品一覧" onClick_displayState={() => displayState()}>
{products.map(product =>
<ProductItem
key={product.id}
product={product}
onAddToCartClicked={() => addToCart(product.id)}
/>
)}
</ProductsList>
)

ProductsContainer.propTypes = {
products: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
price: PropTypes.number.isRequired,
inventory: PropTypes.number.isRequired
})).isRequired,
displayState: PropTypes.func.isRequired,
addToCart: PropTypes.func.isRequired
}

// ここで ステートの商品情報のオブジェクトの配列を
// productsプロップスに結びつける。
const mapStateToProps = state => {
return {
products: getVisibleProducts(state.products)
}
}

export default connect(
mapStateToProps, { displayState, addToCart }
)(ProductsContainer)



実行

ソースコード 03-1


2.cartステートをつくる

カート機能のview、目に見える部分は後回しにして、stateのところを先につくる。


cartリデューサをつくる

addedIds, quantityById を cartリデューサの内部でくくって、ひとまとめにしている。


reducers\cart.js

import {

ADD_TO_CART
} from "../constants/ActionTypes";

// stateのプロパティ
const initialState = {
addedIds: [],
// idをプロパティとするオブジェクトで、数量管理
quantityById: {}
}

// カートに追加されている商品のいdの配列をかえす
const addedIds = (state=initialState.addedIds,action)=>{
switch (action.type) {
case ADD_TO_CART:
if (state.indexOf(action.productId) !== -1) {
return state;
} else {
return [
...state,
action.productId
]
}
default:
return state;
}
}
//商品idをプロパティとする数量を値とするオブジェクト
const quantityById = (state = initialState.quantityById, action) => {
switch (action.type) {
case ADD_TO_CART:
const { productId } = action;
return {
...state,
[productId]: (state[action.productId] || 0) + 1
}
default:
return state;
}
}

//cartリデューサの実体
//内部で addedIds, quantityById リデューサをひとまとめにして利用している
const cart = (state = initialState, action) => {
switch (action.type) {
// ADD_TO_CARTはこちら
default:
return{
addedIds: addedIds(state.addedIds,action),
quantityById: quantityById(state.quantityById,action)
}
}
}

export default cart



index.jsリデューサの修正

すでにつくったproductsリデューサと新しくつくったcartリデューサをひとまとめにしてエクスポート


reducers\index.js

import { combineReducers }  from 'redux';

import products from './products'
import cart from './cart'

export default combineReducers({products,cart});


これだけでcartステートは完成


実行結果

ソースコード 03-2


3.カートのViewをつくる

ショッピングカートを実際にページにくっつけて、cartステートとつないでみる。


Cartコンポーネントをつくる

商品オブジェクトの配列、productsがプロップスとして受け取ると想定。


components\Cart.js

import React from 'react'

import PropTypes from 'prop-types'
import Product from './Product'

const Cart = ({ products}) => {
const hasProducts = products.length > 0
const nodes = hasProducts ? (
products.map(product =>
<Product
title={product.title}
price={product.price}
quantity={product.quantity}
key={product.id}
/>
)
) : (
<em>ご希望の商品をカートに追加してください</em>
)

return (
<div className="border border-dark alert alert-secondary rounded p-1 clearfix mx-auto" style={{width:'30em'}}>
<h5 className="mb-1 text-center">&#x1f6d2;ショッピングカート</h5>
<div>{nodes}</div>
</div>
)
}

Cart.propTypes = {
products: PropTypes.array
}

export default Cart



CartContainerコンテナをつくる

ここでcartステートをコンテナのプロップスにマップして、下層のCartコンポーネントでステートが使えるようにする。

ここで使っている getCartProducts はまだつくっていないので注意。


containers\CartContainer.js

import React from 'react'

import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { getCartProducts } from '../reducers'
import Cart from '../components/Cart'

const CartContainer = ({ products}) => (
<Cart
products={products}
/>
)

CartContainer.propTypes = {
products: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
price: PropTypes.number.isRequired,
quantity: PropTypes.number.isRequired
})).isRequired
}
const mapStateToProps = (state) => ({
products: getCartProducts(state)
})

export default connect(mapStateToProps)(CartContainer)



カートの情報を取得するための補助関数をつくる

cart.addedids と cart.quantityById を取得するための補助関数をつくる。

以下をcartリデューサのファイルに追加


reducers\cart.jsの追加部分


//外部で利用するユーティリティ
export const getAddedIds = state => {
return state.addedIds;
}
//外部で利用するユーティリティ
export const getQuantity = (state, productId) => {
return state.quantityById[productId] || 0
}


ルートのリデューサを編集

関数をいくつか追加している。


reducers\index.js

import { combineReducers } from 'redux';

// この products は、products.js の冒頭の const products ではない。
// combineReducersの戻り値の、 export defaultされたリデューサ
import products, * as fromProducts from './products'
import cart, * as fromCart from './cart'

// カート内のproductsのidを取得
//ここで stateやidと cart.jsリデュース内のユーティリティと関連付け
const getAddedIds = state => fromCart.getAddedIds(state.cart)
const getQuantity = (state, id) => fromCart.getQuantity(state.cart, id)
const getProduct = (state, id) => fromProducts.getProduct(state.products, id)

export default combineReducers({ products, cart });

export const getCartProducts = state => {
return getAddedIds(state).map(id => ({
...getProduct(state, id),
quantity: getQuantity(state, id)
}));
}



CartContainerをDOMに追加


containers\App.js

import React from 'react'

import ProductsContainer from './ProductsContainer'
import CartContainer from './CartContainer';

const App = () => (
<div className="mx-auto" style={{width:"32em"}}>
<h4 className="text-center alert alert-danger">🍓フルーツ市場🍈</h4>
<ProductsContainer />
<CartContainer />
</div>
)

export default App



実行結果

ソースコード 03-3