JavaScript
React
redux

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

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

1. storeをつくる

productsステートなどを保持するstoreをつくる。

商品データを管理するAPIをつくる。

商品のjsonデータを読み込んだり、決裁したりするためのshopオブジェクトをつくる。
サーバーとのやり取りを想定するため、setTimeoutでそれっぽくしているようだ。

api/shop.js
import _products from './products.json'

const TIMEOUT = 100;

export default {
    getProducts: (cb, timeout) => setTimeout( 
        ()=>cb(_products),timeout || TIMEOUT),
    buyProducts: (_payload, cb, timeout) => setTimeout(
        ()=>cb(_products), timeout || TIMEOUT)
}

アクションのtypeを定義

constants/ActionTypes.js
export const RECEIVE_PRODUCTS = 'RECEIVE_PRODUCTS'

対応するアクションを実装

actions/index.js
import shop from '../api/shop'
import * as types from '../constants/ActionTypes'

//引数一個を受ける関数を返す関数。 この引数には実行時、dispatchが割り当てられる。
export const getAllProducts = () => d => {
    // コールバックが引数。実行時、商品データの配列が入る。api/products.json
    shop.getProducts(
        products => d(
            {
                type: types.RECEIVE_PRODUCTS,
                products
            }
        )
    )
}

productsリデューサをつくる

reducers/products.js
import { combineReducers } from 'redux';
import { RECEIVE_PRODUCTS } from '../constants/ActionTypes'

const byId = (state = {}, action) => {
    switch (action.type) {
        case RECEIVE_PRODUCTS:
            return {
                ...state,
                // 配列をidをプロパティにするオブジェクトに加工
                //スプレッド演算子で要素を展開
                ...action.products.reduce(
                    (obj, product) => {
                        obj[product.id] = product;
                        return obj;
                    }, {}
                )
            }
        default:
            return state;
    }
}

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})

リデューサを一本化する準備

のちにcartリデューサもつくるので、そのための準備

reducers/index.js
import { combineReducers }  from 'redux';
import products  from './products'

export default combineReducers({products});

エントリーポイントのindex.jsを修正

index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore, applyMiddleware } from 'redux'
import { Provider } from 'react-redux'
import thunk from 'redux-thunk'
import reducer from './reducers'
import { getAllProducts } from './actions'

const store = createStore(
    reducer,
    applyMiddleware(thunk)
)
store.dispatch(getAllProducts())
store.subscribe(
    () =>console.log(store.getState())
)

ReactDOM.render(
    <Provider store={store}>
    <h1>ダミーH1</h1>
    </Provider>,
    document.getElementById('root')
)

実行結果

2018_0612_1617_55.jpg

ソースコード 02-1

2. コンポーネントを作っていく

productsリデューサに関数追加

商品オブジェクトの配列を返してくれる関数をつくる。
getVisibleProducts関数 と  getProduct関数を追加

reducers\products.js
import { combineReducers } from 'redux';
import { RECEIVE_PRODUCTS } from '../constants/ActionTypes'

const byId = (state = {}, action) => {
    switch (action.type) {
        case RECEIVE_PRODUCTS:
            return {
                ...state,
                // 配列をidをプロパティにするオブジェクトに加工
                //スプレッド演算子で要素を展開
                ...action.products.reduce(
                    (obj, product) => {
                        obj[product.id] = product;
                        return obj;
                    }, {}
                )
            }
        default:
            return state;
    }
}

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) )
}

stateを見れる関数を追加

あとで、ボタンをクリックするとstateを逐次覗けるように自作のdisplayState関数を追加。

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());
  }

Productコンポーネント追加

個々の商品情報を表示する部分

components\Product.js
import React from 'react'
import PropTypes from 'prop-types'

//個々の商品情報の表示部分
const Product = ({ price, quantity, title }) => (
    <div className="clearfix mb-1">
        <span className="p-1  bg-success text-white rounded m1 float-left">{title}</span><span className="w-50 float-right"><span className="float-right w-25 text-right">{quantity ? `  ${quantity}個` : `0個`}</span><span className="float-right w-25 mr-3">&#165;{price}</span></span>
    </div>
)

Product.propTypes = {
    price: PropTypes.number,
    quantity: PropTypes.number,
    title: PropTypes.string
  }

  export default Product

ProductItemコンポーネント追加

個々の商品データを表示、管理する。Productコンポーネントを内包。

components\ProductItem.js
import React from 'react'
import PropTypes from 'prop-types'
import Product from './Product'

//個々の商品データを入れる入れ物
// カートに入れるためのボタンもつける
const ProductItem = ({product})=>(
    <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"
      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
  }

  export default ProductItem

ProductsListコンポーネント追加

商品リストをいれる入れ物。好きなときにstateを覗けるボタン付き。

components\ProductsList.js
import React from 'react'
import PropTypes from 'prop-types'


const ProductsList = ({ title, children,onClick_displayState }) => (
  <div  className="mx-auto">
    <h5 className="text-center">{title}</h5>
    <div>{children}</div>
    <button className="btn btn-primary mx-auto d-block mb-1" style={{width:"6em"}} onClick={onClick_displayState}>state 表示</button>
 </div>
)

ProductsList.propTypes = {
  children: PropTypes.node,
  title: PropTypes.string.isRequired,
  onClick_displayState:PropTypes.func.isRequired
}

export default ProductsList

ProductsContainerコンテナ追加

stateの情報とdispatchを配下のコンポーネントにわたす起点

containers\ProductsContainer.js
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { displayState } from '../actions'
import { getVisibleProducts } from '../reducers/products'
import ProductItem from '../components/ProductItem'
import ProductsList from '../components/ProductsList'


// productsは商品データのオブジェクトの配列。 
// reducers/product//getVisibleProducts関数で取得
// displayStateは stateの中身を覗ける関数。ボタンで使う。

const ProductsContainer = ({ products, displayState }) => (
    <ProductsList title="商品一覧" onClick_displayState={() => displayState()}>
        {products.map(product =>
            <ProductItem
                key={product.id}
                product={product}
            />
        )}
    </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
}

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

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

Appコンテナ追加

あとで、ここにCartコンテナを追加する。

containers\App.js
import React from 'react'
import ProductsContainer from './ProductsContainer'

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

export default App

エントリーポイント修正

index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore, applyMiddleware } from 'redux'
import { Provider } from 'react-redux'
import thunk from 'redux-thunk'
import reducer from './reducers'
import { getAllProducts } from './actions'
import App from './containers/App'

const store = createStore(
    reducer,
    applyMiddleware(thunk)
)
store.dispatch(getAllProducts())
// store.subscribe(
//     () => console.log(store.getState())
// )

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('root')
)

実行結果

2018_0613_1217_20.jpg

この時点では "カートに入れる"は機能しない。"state 表示" でコンポーネントのpropsに情報が伝わっているか確認できる。

ソースコード 02-2