JavaScript + Immer: データ構造をイミュータブルに保つ ー 使うのはイマでしょ！

[Immer](https://immerjs.github.io/immer/)はデータ構造をイミュータブル(不変)に保つためのライプラリです。React公式ドキュメントでもおすすめされています(「[Write concise update logic with Immer](https://beta.reactjs.org/learn/updating-objects-in-state#write-concise-update-logic-with-immer)」参照)。また、2019年にはつぎのような賞を獲得しました。

* [Breakthrough of the year](https://osawards.com/react/2019) ー React open source award
* [The Most Impactful Contribution to the community](https://osawards.com/javascript/2019) ー JavaScript open source award

"[immer](https://kotobank.jp/dejaword/immer)"はドイツ語で「いつも」(always)という意味です。[発音](https://www.howtopronounce.com/immer)は日本語の「今」に近い。ということで、見出しに親父ギャグをぶっこみました。すみません。

Immerはイミュータブルなデータ構造を用いるさまざまな場面で使えます。たとえば、Reactの状態管理はそのひとつです。オブジェクトへの参照が変わっていなければ、オブジェクトもそのまま変更ありません。さらに、複製のコストは比較的低いです。データツリーの中で変更されていない部分は複製されることなく、以前の状態とメモリ上共有されます。

イミュータブルなデータ構造により得られるのがこうした利点です。けれど、手作業でいちいち複製をつくるのは、扱いづらいコードになりやすく、漏れが生じるかもしれません。この課題を解決するのがImmerです。


# インストール
ImmerはES5環境で動きます。インストールはつぎのとおりです。

```terminal
npm install immer
```

YarnやCDNによるインストール、あるいはカスタマイズについては公式サイトの「[Installation](https://immerjs.github.io/immer/installation)」をご参照ください。

# `produce()`関数でつくったミュータブルな状態に変更を加える
Immerの主軸となる関数が[`produce()`](https://immerjs.github.io/immer/produce)です。イミュータブルな(変更しない)状態からつくったミュータブルな(変更できる)状態に変更を加えます。構文はつぎのとおりです。

```javascript
produce(baseState, recipe: (draftState) => void): nextState
```

* `baseState`: `produce`の第1引数で、もととなるイミュータブルな状態。
* `recipe`: `produce`の第2引数となる関数。第1引数から得た状態をどう「変更」するか定める。
* `draftState`: 第2引数の関数`recipe`が受け取る引数。もとの状態`baseState`のプロキシで、安全に変更できる。
* `nextState`: `draftState`に`recipe`の変更を加えた新たな状態。

関数`produce()`はもとの状態`baseState`からつくった`draftState`を`recipe`に引数として渡します。`baseState`はイミュータブルのまま、ミュータブルな`draftState`に`recipe`が変更を加え、`nextState`として返されるのです。

`recipe`の中では、すべての標準JavaScript APIを用いて`draftState`オブジェクトに操作が加えられます。たとえば、フィールドの代入や削除の操作、配列あるいは`Map`、`Set`の変更などです。`push`、`pop`、`splice`、`set`、`sort`、`remove`といった操作も用いることができます。イミュータブルな状態はそのまま保たれますので、参照を書き替えてしまうメソッドも使って構わないのです。

変更できるのは、データツリーのルートにかぎりません。`draftState`の中のどのような深い階層であっても、自由に手が加えられます。

```javascript
draft.todos[0].tags['urgent'].author.age = 56
```

なお、`recipe`関数からの戻り値は、通常はありません。けれど、`draftState`を別のオブジェクトに差し替えたい場合には、返すことができます。詳しくは、「[Returning new data from producers](https://immerjs.github.io/immer/return/)」をご参照ください。

## コード例 

```javascript
import produce from 'immer';

const initialTodos = [
	{ id: 0, title: 'Learn React', done: true },
	{ id: 1, title: 'Try immer', done: false }
];
const nextTodos = produce(initialTodos, (draft) => {
	draft.push({
		id: 2,
		title: 'Learn TypeScript',
		done: false
	});
	draft[1].done = true;
});
```

`console.log()`で確かめると、もとの配列(`initialTodos`)は変わらないまま、複製されたオブジェクト`nextTodos`が変更されています。しかも、手を加えなかった要素(インデックス0)は、オブジェクト間で共有されていることにご注目ください。

```javascript
console.log("length:", initialTodos.length, nextTodos.length);  // length: 2 3
console.log("done:", initialTodos[1].done, nextTodos[1].done);  // done: false true
console.log("shared:", initialTodos[0] === nextTodos[0]);  // shared: true
```


# カリー化した`produce()`関数
簡単な関数のコード例で、`produce()`関数を使ってみましょう。

## スプレッド構文`...`をImmerの`produce()`関数に置き替える
標準JavaScriptで配列やオブジェクトを複製するときは、[スプレッド構文](https://qiita.com/FumioNonaka/items/20e96ca9ede2ae157bcc#%E3%82%B9%E3%83%97%E3%83%AC%E3%83%83%E3%83%89%E6%A7%8B%E6%96%87)がよく用いられます。

```javascript
const todos = [
	{ id: 0, title: 'Learn React', done: true },
	{ id: 1, title: 'Try immer', done: false }
];
const toggleTodo = (todos, id) => {
	const newTodos = [...todos];  // 配列を複製する
	const todo = newTodos.find((todo) => todo.id === id);
	todo.done = !todo.done;
	return newTodos;
};
const nextTodos = toggleTodo(todos, 1);
console.log('nextTodos:', nextTodos[1]);  // nextTodos: {id: 1, title: "Try immer", done: true}
```

`produce()`関数で書き替えたのがつぎのコードです。

```javascript
const toggleTodo = (todos, id) => {
	// const newTodos = [...todos];
	const newTodos = produce(todos, (draft) => {
		const todo = todos.find((todo) => todo.id === id);
		if (!todo) return;
		todo.done = !todo.done;
	});

};
```


# `produce()`関数の第1引数に状態変数の関数を渡す

`produce()`関数の第1引数に関数を渡すと、状態変更の関数として定められて戻り値となります。関数を返すだけですので、実行はされません。戻り値の関数に対して、イミュータブルな状態を第1引数にして呼び出すと、関数による変更が加わったミュータブルな状態として返されるのです。このような構文は「[カリー化](https://ja.wikipedia.org/wiki/%E3%82%AB%E3%83%AA%E3%83%BC%E5%8C%96)」と呼ばれます。

前掲の例にこの構文を用いたのが、つぎのコードです。`produce()`には変更関数(`recipe`)のみ渡して、戻り値の関数を変数(`toggleTodo`)に収めます。そのうえで、第1引数にイミュータブルなオブジェクト(`todos`)を渡して呼び出せば、変更の加わった新たな状態(`nextTodos`)が得られるのです。コードもすっきりと短くなりました。

```javascript
// const toggleTodo = (todos, id) => {
const toggleTodo = produce((draft, id) => {
	// const newTodos = produce(todos, (draft) => {
	const todo = draft.find((todo) => todo.id === id);
	if (!todo) return;
	todo.done = !todo.done;
});
const nextTodos = toggleTodo(todos, 1);
console.log('nextTodos:', nextTodos[1]);  // nextTodos: {id: 1, title: "Try immer", done: true}
```


# さまざまな更新方法
標準JavaScriptだけでイミュータブルなデータ構造を扱おうとすると、データに応じて処理に工夫が必要でした。Immerを使えば、標準JavaScript APIの普通の操作でそれができてしまうのです。

## オブジェクトの変更

```javascript
import produce from 'immer';

const todosObj = {
	id01: { title: 'Learn React', done: true },
	id02: { title: 'Try immer', done: false }
};
// 追加
const addedTodosObj = produce(todosObj, (draft) => {
	draft['id03'] = {
		title: 'Learn TypeScript',
		done: false
	};
});
console.log('added:', addedTodosObj.id03); // added: {title: 'Learn TypeScript', done: false}
// 削除
const deletedTodosObj = produce(todosObj, (draft) => {
	delete draft['id01'];
});
console.log('deleted:', deletedTodosObj.id01); // deleted: undefined
// 更新
const updatedTodosObj = produce(todosObj, (draft) => {
	draft['id01'].done = false;
});
console.log('updated:', updatedTodosObj.id01); // updated: {title: 'Learn React', done: false}
```

## 配列の変更

```javascript
import produce from 'immer';

const todosArray = [
	{ id: 0, title: 'Learn React', done: true },
	{ id: 1, title: 'Try immer', done: false }
];
// 追加
const addedTodosArray = produce(todosArray, (draft) => {
	draft.push({ id: 2, title: 'Learn TypeScript', done: false });
});
console.log('added:', addedTodosArray[2]); // added: {id: 2, title: 'Learn TypeScript', done: false}
// インデックスによる削除
const deletedTodosArray = produce(todosArray, (draft) => {
	draft.splice(1 /* インデックス */, 1);
});
console.log('deleted:', deletedTodosArray[1]); // deleted: undefined
// インデックスによる更新
const updatedTodosArray = produce(todosArray, (draft) => {
	draft[1].done = true;
});
console.log('updated:', updatedTodosArray[1]); // updated: {id: 1, title: 'Try immer', done: true}
// インデックスに挿入
const insertedTodosArray = produce(todosArray, (draft) => {
	draft.splice(2, 0, { id: 2, title: 'Learn TypeScript', done: false });
});
console.log('inserted:', insertedTodosArray[2]); // inserted: {id: 2, title: 'Learn TypeScript', done: false}
// 最後の要素を削除
const deletededLastTodosArray = produce(todosArray, (draft) => {
	draft.pop();
});
console.log(
	'deleted last:',
	deletededLastTodosArray[deletededLastTodosArray.length - 1]
); // deleted last: {id: 0, title: 'Learn React', done: true}
// 最初の要素を削除
const deletedFirstTodosArray = produce(todosArray, (draft) => {
	draft.shift();
});
console.log('deleted first:', deletedFirstTodosArray[0]); // deleted first: {id: 1, title: 'Try immer', done: false}
// 最初の要素として追加
const addedFirstTodosArray = produce(todosArray, (draft) => {
	draft.unshift({ id: 2, title: 'Learn TypeScript', done: false });
});
console.log('added first:', addedFirstTodosArray[0]); // added first: {id: 2, title: 'Learn TypeScript', done: false}
// インデックスによる削除
const deletedByIndexTodosArray = produce(todosArray, (draft) => {
	const index = draft.findIndex((todo) => todo.id === 0);
	if (index !== -1) draft.splice(index, 1);
});
console.log('deleted by index:', deletedByIndexTodosArray[0]); // deleted by index: {id: 1, title: 'Try immer', done: false}
// インデックスを確認して更新
const updatedByIndexTodosArray = produce(todosArray, (draft) => {
	const index = draft.findIndex((todo) => todo.id === 0);
	if (index !== -1) draft[index].done = false;
});
console.log('updated by index:', updatedByIndexTodosArray[0]); // updated by index: {id: 0, title: 'Learn React', done: false}
// 要素のフィルタリング
const filteredTodosArray = produce(todosArray, (draft) => {
	// Array.prototype.filter()メソッドは新たな配列を返す
	// そのため、この場合produce()を用いる必要はない
	// ただし、使うことが有効な場合もある
	return draft.filter((todo) => todo.done);
});
console.log('filtered:', filteredTodosArray);
/*
filtered: (1) [Object]
0: Object
	id: 0
	title: 'Learn React'
	done: true
*/
```


## 入れ子のデータ構造の変更
データを`Map`や`Set`で操作するときは、`enableMapSet`を`import`して呼び出してください(「[Pick your Immer version](https://immerjs.github.io/immer/installation#pick-your-immer-version)」参照)。

```javascript
import produce, { enableMapSet } from 'immer';

enableMapSet();
const todoCollection = {
	index: new Map([
		[
			'01',
			{
				language: 'JavaScript',
				todos: [{ id: 0, title: 'Learn React', done: true }]
			}
		]
	])
};
// 更新
const updatedTodos = produce(todoCollection, (draft) => {
	draft.index.get('01').todos[0].done = false;
});
console.log('updated:', updatedTodos.index.get('01').todos);
/*
updated: (1) [Object]
0: Object
	id: 0
	title: 'Learn React'
	done: false
*/
// フィルタリング
const filteredTodos = produce(todoCollection, (draft) => {
	const selectedTodo = draft.index.get('01');
	const todos = selectedTodo.todos.filter((todo) => todo.done);
});
console.log('filtered:', filteredTodos.index.get('01').todos);
/*
filtered: (1) [Object]
0: Object
	id: 0
	title: 'Learn React'
	done: true
*/
```

配列に複数の要素を加えたいときは、メソッドによっては複数の引数で渡したり、スプレッド構文`...`が使えます。

```javascript
todos.unshift(...items)
```

idで識別されるオブジェクトの配列を操作する場合には、上記コード例のように`Map`やインデックスにもとづいて操作することがお勧めです。いちいち要素を検索するより、パフォーマンスは高まります。

公式ドキュメントの「[Basics](https://immerjs.github.io/immer/)」をもとに、Immerの基本についてご説明しました。`produce()`関数の使い方を覚えれば、あとは標準のJavaScript APIで操作すればよいというのがお手軽です。