LoginSignup
1
0

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-06-13

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

1
0
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
1
0