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

[Reduxのサンプルのショッピングカートをステップ・バイ・ステップで Part.2](https://qiita.com/tkarasuma/items/e7c0a2a7c46962091c05)のつづき。

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

###アクションのtypeを追加

```javascript:constants\ActionTypes.js
export const RECEIVE_PRODUCTS = 'RECEIVE_PRODUCTS'
export const ADD_TO_CART = 'ADD_TO_CART'
```

###ADD_TO_CARTアクションを発行する

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

```javascript: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の内部で利用できるようにしている。

```javascript: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)の実行を命令する関数。

```jsx: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関数の第２引数のオブジェクトにaddToCartを追加するのを忘れないよう注意。


```jsx: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)
```

###実行

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">Reduxサンプルのショッピングカートをステップ・バイ・ステップで Part.3-1 <a href="https://t.co/3FDUZvk0xc">https://t.co/3FDUZvk0xc</a> <a href="https://twitter.com/YouTube?ref_src=twsrc%5Etfw">@YouTube</a>さんから</p>&mdash; tkarasuma (@tkarasuma) <a href="https://twitter.com/tkarasuma/status/1006800243332087808?ref_src=twsrc%5Etfw">2018年6月13日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


[ソースコード　03-1](https://github.com/tkarasuma/JavaScript/tree/master/Redux_shopping_cart_03-1)

#2.cartステートをつくる

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

###cartリデューサをつくる

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

```javascript:reducers\cart.js
import {
    ADD_TO_CART
} from "../constants/ActionTypes";

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

// カートに追加されている商品のいｄの配列をかえす
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リデューサをひとまとめにしてエクスポート

```javascript:reducers\index.js
import { combineReducers }  from 'redux';
import products  from './products'
import cart from './cart'

export default combineReducers({products,cart});
```
これだけでcartステートは完成

###実行結果

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">Reduxのサンプルのショッピングカートをステップ・バイ・ステップで Part.3-2 <a href="https://t.co/3YNbeRN0Fi">https://t.co/3YNbeRN0Fi</a> <a href="https://twitter.com/YouTube?ref_src=twsrc%5Etfw">@YouTube</a>さんから</p>&mdash; tkarasuma (@tkarasuma) <a href="https://twitter.com/tkarasuma/status/1006790736212262912?ref_src=twsrc%5Etfw">2018年6月13日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

[ソースコード 03-2](https://github.com/tkarasuma/JavaScript/tree/master/Redux_shopping_cart_03-2)

#3.カートのViewをつくる

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

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

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

```jsx: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 はまだつくっていないので注意。

```jsx: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リデューサのファイルに追加

```javascript:reducers\cart.jsの追加部分

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

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

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

```javascript: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に追加

```jsx: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

```

###実行結果

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">Reduxのサンプルのショッピングカートをステップ・バイ・ステップで Part.3-3 <a href="https://t.co/MWTF2zod21">https://t.co/MWTF2zod21</a> <a href="https://twitter.com/YouTube?ref_src=twsrc%5Etfw">@YouTube</a>さんから</p>&mdash; tkarasuma (@tkarasuma) <a href="https://twitter.com/tkarasuma/status/1006798708036571137?ref_src=twsrc%5Etfw">2018年6月13日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

[ソースコード 03-3](https://github.com/tkarasuma/JavaScript/tree/master/Redux_shopping_cart_03-3)