Reduxのサンプルのショッピングカートをステップ・バイ・ステップで Part.2のつづき。
1.カートに入れるアクション追加
アクションのtypeを追加
export const RECEIVE_PRODUCTS = 'RECEIVE_PRODUCTS'
export const ADD_TO_CART = 'ADD_TO_CART'
ADD_TO_CARTアクションを発行する
ADD_TO_CARTアクションを発行するaddToCart関数をつくる。
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の内部で利用できるようにしている。
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)の実行を命令する関数。
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を追加するのを忘れないよう注意。
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)
実行
Reduxサンプルのショッピングカートをステップ・バイ・ステップで Part.3-1 https://t.co/3FDUZvk0xc @YouTubeさんから
— tkarasuma (@tkarasuma) 2018年6月13日
2.cartステートをつくる
カート機能のview、目に見える部分は後回しにして、stateのところを先につくる。
cartリデューサをつくる
addedIds, quantityById を cartリデューサの内部でくくって、ひとまとめにしている。
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リデューサをひとまとめにしてエクスポート
import { combineReducers } from 'redux';
import products from './products'
import cart from './cart'
export default combineReducers({products,cart});
これだけでcartステートは完成
実行結果
Reduxのサンプルのショッピングカートをステップ・バイ・ステップで Part.3-2 https://t.co/3YNbeRN0Fi @YouTubeさんから
— tkarasuma (@tkarasuma) 2018年6月13日
3.カートのViewをつくる
ショッピングカートを実際にページにくっつけて、cartステートとつないでみる。
Cartコンポーネントをつくる
商品オブジェクトの配列、productsがプロップスとして受け取ると想定。
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">🛒ショッピングカート</h5>
<div>{nodes}</div>
</div>
)
}
Cart.propTypes = {
products: PropTypes.array
}
export default Cart
CartContainerコンテナをつくる
ここでcartステートをコンテナのプロップスにマップして、下層のCartコンポーネントでステートが使えるようにする。
ここで使っている getCartProducts はまだつくっていないので注意。
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リデューサのファイルに追加
//外部で利用するユーティリティ
export const getAddedIds = state => {
return state.addedIds;
}
//外部で利用するユーティリティ
export const getQuantity = (state, productId) => {
return state.quantityById[productId] || 0
}
ルートのリデューサを編集
関数をいくつか追加している。
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に追加
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
実行結果
Reduxのサンプルのショッピングカートをステップ・バイ・ステップで Part.3-3 https://t.co/MWTF2zod21 @YouTubeさんから
— tkarasuma (@tkarasuma) 2018年6月13日