LoginSignup
3
2

More than 5 years have passed since last update.

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

Posted at

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

3
2
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
3
2