よくあるオブジェクトが要素のリストでオブジェクトの内容を変更したい。
そして配列は immutable に扱いたい。
interface Idol { id: number; name: string; type: string; unit?: string; }; const data: Idol[] = [ { id: 1, name: 'Hosimiya Ichigo', type: 'cute', unit: 'soleil' }, { id: 2, name: 'Kiriya Aoi', type: 'cool', unit: 'soleil' }, { id: 3, name: 'Shibuki Ran', type: 'sexy' }, { id: 4, name: 'Todo Yurika', type: 'cool' }, { id: 5, name: 'Kanzaki Mistuki', type: 'sexy' }, { id: 6, name: 'Nastuki Mikuru', type: 'pop' }, { id: 7, name: 'Ozora Akari', type: 'cute' }, ];
map で変更する
const updateList = (list) => (id, data) => { return list.map((item) => { if (item.id === id) { return { ...item, ...data, } } return item; }); }; const newData = updateList(data)(3, { unit: 'soleil' }); /* [ {id: 1, name: "Hosimiya Ichigo", type: "cute", unit: "soleil"}, {id: 2, name: "Kiriya Aoi", type: "cool", unit: "soleil"}, {id: 3, name: "Shibuki Ran", type: "sexy", unit: "soleil"}, {id: 4, name: "Todo Yurika", type: "cool"}, {id: 5, name: "Kanzaki Mistuki", type: "sexy"}, {id: 6, name: "Nastuki Mikuru", type: "pop"}, {id: 7, name: "Ozora Akari", type: "cute"} ] */
配列をすべてループで回すので扱うデータが大きいとパフォーマンスに影響がありそうだけど、シンプルにデータの変更ができる
更新するデータのインデックスを取得して、更新データの前後を slice で取り出して結合する
const updateList = (list) => (id, data) => { const index = list.findIndex((item) => item.id === id); // 変更対象がない時はコピーした元データを返す if (index === -1) { return [...list]; } return [ ...list.slice(0, index), { ...list[index], ...data, }, ...list.slice(index + 1), ]; }; updateList(data)(7, { unit: 'luminas' }); /* [ {id: 1, name: "Hosimiya Ichigo", type: "cute", unit: "soleil"}, {id: 2, name: "Kiriya Aoi", type: "cool", unit: "soleil"}, {id: 3, name: "Shibuki Ran", type: "sexy"}, {id: 4, name: "Todo Yurika", type: "cool"}, {id: 5, name: "Kanzaki Mistuki", type: "sexy"}, {id: 6, name: "Nastuki Mikuru", type: "pop"}, {id: 7, name: "Ozora Akari", type: "cute", unit: "luminas"} ] */
Array.prototype.slice
メソッドは取り出せる要素がない時、空配列 []
を返すので変更対象が配列の先頭でも最後でもエラーにならない
e.g.
const list = [1, 2, 3, 4, 5] list.slice(0, 0); // => [] list.slice(list.length); // => []
id をキーにしたオブジェクトを作成して、変更後に配列に戻す
// 配列を key をキーにしたオブジェクトに変換する const mapToObjectbyKeys = (list) => (key) => { return list.reduce((obj, item) => { return { ...obj, [item[key]]: {...item} } }, {}); }; // キーのリスト順にオブジェクトを配列に戻す const objectToArrayByList = (obj) => (keyList) => { return keyList.map((key) => { return {...obj[key]}; }); }; const updateList = (list) => (key, id, data) => { // key のリストを作成 const keyList = list.map((item) => item[key]); // key をキーにしたオブジェクトを作成 const dataObject = mapToObjectbyKeys(list)(key); // データを更新したオブジェクトを作成 const updateData = { ...dataObject, [id]: { ...dataObject[id], ...data, } }; // 配列に戻して返す return objectToArrayByList(updateData)(keyList); }; updateList(data)('id', 5, { unit: 'tristar' }); /* [ {id: 1, name: "Hosimiya Ichigo", type: "cute", unit: "soleil"}, {id: 2, name: "Kiriya Aoi", type: "cool", unit: "soleil"}, {id: 3, name: "Shibuki Ran", type: "sexy"}, {id: 4, name: "Todo Yurika", type: "cool"}, {id: 5, name: "Kanzaki Mistuki", type: "sexy", unit: "tristar"}, {id: 6, name: "Nastuki Mikuru", type: "pop"}, {id: 7, name: "Ozora Akari", type: "cute"} ] */
やりたいことに対して壮大すぎる気がする…
内部的なデータはオブジェクトに変換したもので持っておき、インデックス順に並べて表示する時だけ配列に戻すという運用方法ならありかもしれない。
関数内で配列のコピーを作成してしまう
immutable 原理主義でないなら配列を丸っとコピーして、関数内で破壊的なメソッドを使って変更して返してしまえば簡単そう
コピーした配列を直接操作する
const updateList = (_list) => (id, data) => { const list = [..._list]; const index = list.findIndex((item) => item.id === id); if (index === -1) { return list; } const item = list[index]; list[index] = { ...item, ...data, }; return list; }; updateList(data)(6, { unit: 'WM' }); /* [ {id: 1, name: "Hosimiya Ichigo", type: "cute", unit: "soleil"}, {id: 2, name: "Kiriya Aoi", type: "cool", unit: "soleil"}, {id: 3, name: "Shibuki Ran", type: "sexy"}, {id: 4, name: "Todo Yurika", type: "cool"}, {id: 5, name: "Kanzaki Mistuki", type: "sexy"}, {id: 6, name: "Nastuki Mikuru", type: "pop", unit: "WM"}, {id: 7, name: "Ozora Akari", type: "cute"}, ] */
splice を使うパターン
const updateList = (_list) => (id, data) => { const list = [..._list]; const index = list.findIndex((item) => item.id === id); if (index === -1) { return list; } // 置き換えられる要素が返される list.splice(index, 1, { ...list[index], ...data, }); return list; }; updateList(data)(1, { unit: 'Cosmos' }); /* [ {id: 1, name: "Hosimiya Ichigo", type: "cute", unit: "Cosmos"}, {id: 2, name: "Kiriya Aoi", type: "cool", unit: "soleil"}, {id: 3, name: "Shibuki Ran", type: "sexy"}, {id: 4, name: "Todo Yurika", type: "cool"}, {id: 5, name: "Kanzaki Mistuki", type: "sexy"}, {id: 6, name: "Nastuki Mikuru", type: "pop"}, {id: 7, name: "Ozora Akari", type: "cute"} ] */
Array.prototype.splce()
は破壊メソッドで、戻り地は変更される値なのでそのまま return はできない
for break を使う
配列をコピーしてしまっているので for で回して変更対象のデータを置き換えたら break してしまうのもアリかも
const updateList = (_list) => (id, data) => { const list = [..._list]; for(let i = 0, l = list.length; i < l; i += 1) { if (list[i].id === id) { list[i] = { ...list[i], ...data, }; break; } } return list; }; updateList(data)(4, { name: 'Arisugawa Otome', type: 'pop' }); /* [ {id: 1, name: "Hosimiya Ichigo", type: "cute", unit: "soleil"}, {id: 2, name: "Kiriya Aoi", type: "cool", unit: "soleil"}, {id: 3, name: "Shibuki Ran", type: "sexy"}, {id: 4, name: "Arisugawa Otome", type: "pop"}, {id: 5, name: "Kanzaki Mistuki", type: "sexy"}, {id: 6, name: "Nastuki Mikuru", type: "pop"}, {id: 7, name: "Ozora Akari", type: "cute"} ] */
伝統的な書き方って感じだけどOK。
所管
データがめちゃめちゃ大きくならないなら map
を使うのがわかりやすそう。
それか配列を丸っとコピーしてしまって、findIndex で変更データを探して置き換えるパターンが見やすいのかな?
Array.prototype.splice
が変更された新しい配列を返してくれるメソッドなら楽だったのにな…って感じ。
lodash とかライブラリを使うといい感じに操作できる関数があるのかもしれない。
TODOアプリとか結構あるあるなデータ変更だと思うんだけど、みんなどうしてるのか教えて〜
おわり
[参考]
- Array.prototype.map() - JavaScript | MDN
- Array.prototype.findIndex() - JavaScript | MDN
- Array.prototype.slice() - JavaScript | MDN
- Array.prototype.findIndex() - JavaScript | MDN
- Array.prototype.splice() - JavaScript | MDN
- Lodash
- 作者:あんどうやすし
- 発売日: 2020/11/17
- メディア: 単行本(ソフトカバー)
TVアニメ/データカードダス アイカツ! ベストアルバム Calendar Girls
- アーティスト:STAR☆ANIS
- 発売日: 2014/04/09
- メディア: CD