かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

JavaScript 配列の要素を immutable に変更する方法を考えてみる

よくあるオブジェクトが要素のリストでオブジェクトの内容を変更したい。
そして配列は 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アプリとか結構あるあるなデータ変更だと思うんだけど、みんなどうしてるのか教えて〜

おわり


[参考]

ハンズオンJavaScript

ハンズオンJavaScript

道に迷ったときはアイカツ!を見よう