immerという、ミュータブルな操作を書く感覚でイミュータブルな操作が行えるライブラリがあります。
import {produce} from 'immer'; // 破壊的に配列をシャッフルする関数 const shuffle=(a,i=a.length,j) => {for(;[a[i],a[j]]=[a[j=0|Math.random()*i],a[--i]],i;);}; const arr = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ] const shuffledArray = produce(arr, (draftArray) => { shuffle(draftArray); }); // シャッフルされた配列が得られる
以前のimmerはネイティブの配列とオブジェクトしか扱えませんでした。しかし、久しぶりに見てみたらインスタンスにも適用できるようになっていました(ほとんどのインスタンスは対応していますが、すべてに対応しているわけではないです)。
1. インスタンスに対応
immerのインスタンス対応はMyClass.prototype[immerable] = true
か、myInstance[immerable] = true
とするだけで対応できます。
import {produce, immerable} from 'immer'; class Vector{ constructor(x, y) { this.x = x; this.y = y; } setZ(z) { this.z = z; } } const vector = new Vector(1, 2); vector[immerable] = true; // これだけでimmerで使えるようになります const after = produce(vector, draft => { draft.x += 4; draft.y += 4; draft.setZ(7); }); console.log(vector, after);
immerableはprototypeに直接設定しても問題ありません。(というかこっちのほうが恐らく主流です)
Vector.prototype[immerable] = true;
クラスがgetterやsetterを使っている場合には使えないようです。
2. draftを外部に出せるcreateDraft()
draftを外に出すと、ネストが浅くなるだけでなく既存のコードをほんの少し変えるだけでimmutable化できるようになります。たとえばshuffle()という破壊的変更を加える関数を考えます。
const shuffle = (arr) => { // 破壊的にarrをシャッフルする for (let i = arr.length - 1; i > 0; i = 0 | i - 1) { const j = 0 | Math.random() * (i+1); [arr[i], arr[j]] = [arr[j], arr[i]]; } }; const arr = [1, 2, 3, 4, 5]; shuffle(arr);
このコードにcreateDraftを加えてちょっと書き直せば、arrが破壊されなくなります。
import {createDraft, finishDraft} from 'immer'; const shuffle = (arr) => { // 破壊的にarrをシャッフルする for (let i = arr.length - 1; i > 0; i = 0 | i - 1) { const j = 0 | Math.random() * (i+1); [arr[i], arr[j]] = [arr[j], arr[i]]; } }; const arr = [1, 2, 3, 4, 5]; const draft = createDraft(arr); // 追加 shuffle(draft); // draftを渡すように変更する const shuffledArr = finishDraft(draft); // 追加
ただ、個人的にはネストされたほうが「このエリアはimmerのエリアだな」と分かりやすくなるのでcreateDraftよりproduceが好きです。