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')
)
実行結果
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">¥{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')
)
実行結果
この時点では "カートに入れる"は機能しない。"state 表示" でコンポーネントのpropsに情報が伝わっているか確認できる。