Reactもだいぶ、バージョン16.8以降、すなわち関数コンポーネントでの記述方法が主流となってきた気がします。そして、この関数コンポーネントの記法になってから、データ処理、同期に際してhooks(useStateなど)と同時に重要な要素として、採り上げておくべき事項があると感じました。
それは、分割代入をよく把握しておかないと能率的なデータの同期が取れないということです。特に、データ修正に際しては過去に情報が錯綜していたこともあり、それに特化して記事にしてみました。
買い物かごを作ってみる
では、演習用のプログラムとして単純な買い物かごシステムを作ってみます。仕様は以下の通りです。
- 商品群productsに対し、商品をリスト化する。
- 買い物かごに追加ボタンをクリックすると、買い物かごリストに追加される。
- 同じ商品が追加された場合、買い物かごリストの個数が加算される。
- 買い物かごリストから取消ボタンがクリックされた場合、商品を買い物かごリストから抹消する。
それで作ってみた演習用プログラムが以下の通りです。
参考ソースはこちら Build a React Hooks Shopping Cart with useState and useEffect
※Googleでshopping cart react hooksで検索すると色々出てくるので、参考になります。
import React, { useState} from "react"
const ShoppingCart = ()=>{
const [cart, setCart] = useState([])
const products = [
{
id: 1,
name: "SHURE SRH1840",
price: 59800,
},
{
id: 2,
name: "Zenhizer HD650",
price: 46000,
},
{
id: 3,
name: "HIFIMAN SUNDARA",
price: 38000,
},
]
const total = cart.reduce((total,{price = 0,unit})=> total + price * unit,0)
const addToCart = (el)=>{
const updCart = [...cart]
let sameindex = updCart.findIndex((item)=> item.id === el.id)
if(sameindex >= 0 ){
const edit = {...updCart[sameindex]}
edit.unit++
updCart[sameindex] = edit
}else{
updCart.push({...el,unit: 1})
}
setCart(updCart)
}
const removeFromCart = (el)=>{
let hardCopy = [...cart]
hardCopy = hardCopy.filter((cartItem)=> cartItem.id !== el.id)
setCart(hardCopy)
}
const productItems = products.map((el)=>(
<div key={el.id}>
{`${el.name}:${el.price}円`}
<input type="submit" value="買い物かごへ" onClick={()=> addToCart(el)} />
</div>
))
const cartItems = cart.map((el)=>(
<div key={el.id}>
{`${el.name}: ${el.price}円: 個数 ${el.unit}`}
<input type="submit" value="取消" onClick={()=> removeFromCart(el)} />
</div>
))
return(
<div>
STORE
<div>{productItems}</div>
<div>買い物かごリスト</div>
<hr/>
<div>{cartItems}</div>
<div>総額: {total}</div>
</div>
)
}
export default ShoppingCart
更に詳しく
ここがこの記事で特別採り上げておきたいデータの更新部分で、分割代入を効率的に用いることで、ここまでスムーズに記述できます。
const updCart = [...cart] //買い物かごcartを分割代入し、同期用のオブジェクトupdCartを作成
let sameindex = updCart.findIndex(item => item.id === el.id) //既存商品のインデックス抽出
if(sameindex >= 0 ){
const edit = {...updCart[sameindex]} //既存商品の抽出
edit.unit++ //個数の加算
updCart[sameindex] = edit //値の更新
}else{
updCart.push({...el,unit: 1}) //選択商品の新規挿入
}
setCart(updCart) //同期処理
データの更新と同期
ここで{...el,unit: 1}
のおさらいで、これはJavaScriptの分割代入です。その基本となる形、{...hoge,fuga}についてですが、
hogeという元のオブジェクトに対し、プロパティfugaが持っている値を更新する
こういう役割を持っています。更に三点のドットが付与された...hoge
ですが、これはスプレッド構文と呼び、不足している配列情報を補完する
役割を持ちます。具体的には、el
は元の配列cartをmapメソッドによってループ展開させたうちの一要素(買い物かご変数cartに格納された商品情報の一つ)に過ぎません。ですが、その指定の要素(更新する必要があるのは個数を表すプロパティのunitのみ)を更新するために、更新対象でない要素(買い物かごに格納したcartが持っている商品情報のid、name、price)も逐一更新情報に明記するのは面倒なことこの上ありません。そこで、このスプレッド構文を活用することで、更新される値以外の要素を補完することができるわけです。
なので、まずは[...cart]
によって(更新の有無にかかわらず)cart内の全データを取得し、参照渡しによって得られた変数updCartに格納させておきます。
続いて{...updCart[sameindex]}
で選択された買い物かご内の商品情報(sameindexは選択された商品かごの番号が入る)に該当する商品情報の全プロパティ(更新の有無にかかわらず)を取得し、それを変数editに参照渡し、更新された変数editをupdCartに代入しています。
ただ、これだとまだ同期が取れないので、useStateフックに設定したsetCartメソッドで同期を取り、これでリアルタイムなオブジェクトの値の修正がスムーズに行うことができます。ちなみにuseStateフックも分割代入を用いたものです。
処理の分岐
データ更新の基本の仕組みがわかったところで、具体的な処理を見ていきます。このシステムでは新規に商品情報を挿入する場合と、既存の買い物かご内の情報を更新する場合に分けています。その鍵となる記述が以下の部分です。
let sameindex = updCart.findIndex(item => item.id === el.id)
これは買い物かごcartを分割代入した更新用オブジェクトupdCart(現時点ではcartと同じ値、すなわち初期動作だと中身は[]となっている)に対し、item.idとel.id(選択した商品のid)が一致しているインデックスを取得してきます。初期動作だと何も入っていないため、戻り値は-1を返してくれるので、新規作成に分岐が振り分けられます。
さて、同じ商品をもう一度押した場合は買い物かご情報cart内に格納された商品番号を検出してくれるため、その買い物かご内の商品に対し、更新処理が行われることになります。それが以下の部分です。まずは対象商品のインデックス番号が取得できたので、それに該当する商品を分割代入して編集用変数editに参照渡しで代入します。したがって、このeditの値のみ更新可能となるので、ここで値のインクリメント処理を行います。そして、もう一度updCart[sameindex]に代入することで、このupdCart内のプロパティunitの値はインクリメント(加算)されていきます。
const edit = {...updCart[sameindex]} //既存商品をスプレッド構文で抽出。editには更新対象外のデータも格納されることになる
edit.unit++ //値のインクリメント //edit内のunitプロパティの値を+1で刷新
updCart[sameindex] = edit //値の更新
逆に、別の新しい商品が買い物かごに入った場合は、上記の分岐で-1を示す(商品番号が検出されないため)ので、新規挿入されていきます。
配列をmapで回す場合
配列の中からmapで回す方法もあります。テーブル情報の変更やステータス情報の切替など、既存のデータが必ず存在し、その値を変更したりする場合ならこの方法でもいいでしょう。
このようなサンプルでクリックしたリストのステータスが変化します。
import React, { useState,useEffect } from "react";
const PushBox = ()=>{
const [vals,setVal] = useState([])
const items = [
{"id":1,"name":"札幌"},
{"id":2,"name":"仙台"},
{"id":3,"name":"東京"},
{"id":4,"name":"名古屋"},
{"id":5,"name":"大阪"},
{"id":6,"name":"広島"},
{"id":7,"name":"福岡"},
]
useEffect(()=>{
setVal(items)
},[])
const upd = (sel)=>{
items.map((item,idx)=>{
if(item.id == sel){
const edit = items
item.name = item.name+"を選択!"
edit[idx] = item
setVal(edit)
}
})
}
return(
<ul>
{vals.map((item,idx)=>(
<li key={item.id}>
{item.name}
<input type="button" onClick={ ()=> upd(item.id)} value="push" />
</li>
))}
</ul>
)
}
export default PushBox
}
毎回、選択された都市が入れ替わるのはitemsに随時代入しているからですが、再度別のボタンをクリックすると、リストitemsは初期情報が保持されていないので、逐一入れ替わります。もし、選択した値を保持したい場合は
const edit = [...vals] //常に同期をとっておきたいリストを退避用の変数に分割代入する
item.name = item.name+"を選択" //対象の値を更新する。map関数内の引数から
edit[idx] = item //更新情報を編集用の変数に代入
setVal(edit) //同期処理。必ずmap関数内で行うこと!
このようにすれば、移し替えたリスト情報の分割代入をスプレッド構文で行っているので、選択されていないリスト情報を保持することができます。
useReducerフックを使用する場合
useReducerフックを用いれば、複数のオブジェクトに対して一度に更新処理を実行することができます。ただし、関数コンポーネントでuseReducerフックを使用する場合、useStateフックを使用しないので、呼び出したstateオブジェクトに対して、更新した値を的確に分割代入を施す必要があります。これを間違うと、取りたい変数の同期が取れなくなってしまいます。
例は、ナビゲーションを用いた簡易なショッピングサイトで、それぞれのクリックイベントによって、買い物かごへの追加、買い物かごからの商品返却、購入を制御し、そのイベントごとにメソッドを振り分けています。
このstateプロパティ内には陳列商品を格納するproduct、買い物かごを格納するcartという配列と、買い物かご内の合計金額を格納するtotalという単一の変数(プリミティブ型という)が管理されています。
記述当初はこのようになっており、同期をとる項目が増えるに連れて、最後の分割代入処理の記述行も増えて煩雑になってしまいました。
const addProductToCart = (product, state) => {
let cartIndex = null
//買い物かごの調整
const updatedCart = [...state.cart];
const updatedItemIndex = updatedCart.findIndex(
item => item.id === product.id
);
if (updatedItemIndex < 0) {
updatedCart.push({ ...product, quantity: 1,stock: 0 });
cartIndex = updatedCart.length -1 //カートの最後尾
} else {
const updatedItem = { ...updatedCart[updatedItemIndex] }
updatedItem.quantity++;
updatedCart[updatedItemIndex] = updatedItem;
cartIndex = updatedItemIndex //加算対象のインデックス
}
//商品在庫の調整
const updatedProducts = [...state.products] //商品情報
const productid = updatedCart[cartIndex].id //在庫減算対象の商品
const productIndex = updatedProducts.findIndex(
p => productid === p.id
)
const tmpProduct = { ...updatedProducts[productIndex] }
tmpProduct.stock-- //在庫の減算
updatedProducts[productIndex] = tmpProduct
//合計金額の調整
const summary = getSummary(updatedCart,state.total) //合計値の取得
//stateオブジェクトの同期を取る値
state = {...state, total: summary}
state = { ...state, cart: updatedCart }
state = {...state, products: updatedProducts }
return state
};
後で分割代入を行えばいいのはstateだけだと気づいたので、stateを参照渡しして変数statを作成、そのstatに対し更新処理を行い、最後にそのstatを分割代入すると記述がかなりスリムになりました。
export const ADD_PRODUCT = "ADD_PRODUCT";
export const REMOVE_PRODUCT = "REMOVE_PRODUCT";
export const BUY_IT = "BUY_IT";
const addProductToCart = (product, state) => {
let cartIndex = null
const stat = state //stateを参照渡しで複製
//買い物かごの調整
const updatedCart = stat.cart;
const updatedItemIndex = updatedCart.findIndex(
item => item.id === product.id
);
if (updatedItemIndex < 0) {
updatedCart.push({ ...product, quantity: 1,stock: 0 });
cartIndex = updatedCart.length -1 //カートの最後尾
} else {
const updatedItem = { ...updatedCart[updatedItemIndex] }
updatedItem.quantity++;
updatedCart[updatedItemIndex] = updatedItem;
cartIndex = updatedItemIndex //加算対象のインデックス
}
stat.cart = updatedCart //買い物かご情報(配列)の格納
//商品在庫の調整
const updatedProducts = stat.products //商品情報
const productid = updatedCart[cartIndex].id //在庫減算対象の商品
const productIndex = updatedProducts.findIndex(
p => productid === p.id
)
const tmpProduct = { ...updatedProducts[productIndex] }
tmpProduct.stock-- //在庫の減算
updatedProducts[productIndex] = tmpProduct
stat.products = updatedProducts //在庫情報(配列)の格納
//合計金額の調整
const total = stat.total
const sum = getSummary(updatedCart,total)
stat.total = sum //合計計算の格納
state = {...stat} //statには更新情報とそれ以外の情報が格納されている
return state
};
※処理対象の変数に配列と単一の変数(プリミティブ型)が混ざっている場合、気をつけて分割代入を行わないと、参照渡しの弊害で配列しか同期を取れない現象が起きたりしますが、これはプリミティブ型の変数に参照渡しする場合と、配列に値を参照渡しする場合の挙動が異なるためです。