例えば、こういうリストを持つStateがあったときに、
const [sampleData, setSampleData] = useState<number[]>([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
リスト表示して、そのうちのどれかを加算できるようにしようとすると、最初こういうコードを書いていた。
<div> {sampleData.map((item, index) => ( <div key={index}>{item} <button onClick={() => { setSampleData((origin) => { origin[index] = origin[index] + 1 return [...origin] }) }}>+</button></div> ))} </div>
しかしどうもいけてない気がする。気に入らないのは origin[index] = origin[index] + 1の部分だ。originを変更してしまうことに気持ち悪さがあるのである。
では、
const copy = [...origin] copy[index] = copy[index] + 1 return [...copy]
こうしてはどうか。不要な配列コピーが一つできてしまうけど、もとの配列を壊さないぞという意思は伝わるような気がする。しかし結局、配列の要素に対する代入があるのが嫌なので、気持ち悪さは解消しない。
世の中の人はどうやってるのかな、といくつか見て回ったら、
return [...origin.slice(0, index), origin[index] + 1, ...origin.slice(index + 1)]
あーはん。記述はちょっと煩雑ではあるけど、配列要素への代入を避けたい意思はちゃんと伝わるようになっている。でも配列生成コストはslice 2回呼んでる分増えはする気がする? 気にするほどでもないか。
最初 slice ってメソッド名から、なるほど新しい配列を作らずビューを返すのでコピーコストがかからないんだなと勝手に早合点していたけど、
slice() は Array インスタンスのメソッドで、配列の一部を start から end (end は含まれない)までの範囲で、選択した新しい配列オブジェクトにシャローコピーして返します。 ここで start と end はその配列に含まれる項目のインデックスを表します。元の配列は変更されません。
なので、仕様上はそういう最適化は明らかではなさそう。(ランタイムが関数内で読み取り歯科されてない場合は本当のコピーは行わないなどの最適化はしそうな気はするけども)
なお Array#slice(n)は n が配列サイズを超えている場合も OutOfIndex 的な落ち方をせず空配列を返すため、末尾要素を指定した場合でも問題は起きない。
関係ないけど、自分は本番系にこういう処理を書いてリリースしたときに sliceを splice に書き間違えて悲しい目にあったことがある。動的に書き換えが走る場合で、一つのテキストに複数回書き換えが走るパターンをテストしてなかったのだった。