ReactでTrelloみたいなToDoリスト＜4＞ immutability-helper


## 関連記事

* [Reactのスタート](http://qiita.com/park-jh/items/63bf0f013c998c5572aa)
* [Reactのプロジェクト](http://qiita.com/park-jh/items/488d43e2d0b87991207e)
* [Reactで挨拶](http://qiita.com/park-jh/items/bffaec5d05a96850e91b)
* [ReactでTrelloみたいなToDoリスト＜1＞ props](http://qiita.com/park-jh/items/f3f6162cee982eb9e6f3)
* [ReactでTrelloみたいなToDoリスト＜2＞ state](http://qiita.com/park-jh/items/0b71fd9c7e6cc8cbe844)
* [ReactでTrelloみたいなToDoリスト＜3＞ prop-types](http://qiita.com/park-jh/items/6a8eac233e3f3ae61f32)
* ReactでTrelloみたいなToDoリスト＜4＞ immutability-helper

## 外部APIとの通信、Props Callback function

今までは外観を作っただけで、カードの操作はできなかった。
カードの操作ができるように機能を追加してみよう。
また著者のサイトから学習用のAPIサーバーを提供しているので、そのサーバーと通信する形で改善する。
カードを操作する機能については、親コンポーネントからfunctionを作り、Props Callbackとして子コンポーネントへ渡す。

```diff:src/index.js
diff --git a/src/index.js b/src/index.js
index dd395b8..6e38759 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,6 +1,6 @@
 import React from 'react';
 import ReactDOM from 'react-dom';
-import KanbanBoard from './components/KanbanBoard';
+import KanbanBoardContainer from './components/KanbanBoardContainer';
 import registerServiceWorker from './registerServiceWorker';
 import './index.css';

@@ -39,5 +39,5 @@ const cardsList = [
   },
 ];

-ReactDOM.render(<KanbanBoard cards={cardsList} />, document.getElementById('root'));
+ReactDOM.render(<KanbanBoardContainer cards={cardsList} />, document.getElementById('root'));
 registerServiceWorker();
```

外部APIとの通信でカードリストを取ってくるのでcardsListという仮のデータは要らないが、そのまま置いて進む。
もちろん消しても構わない。

ここからが本番だ。
カードを操作するためにReactから提供している[immutability-helper](https://github.com/kolodny/immutability-helper)を使う。
本でreact-addons-updateを紹介しているが、”legacyだから代わりにimmutability-helperを使ってくれ”と公式ドキュメントに書かれていた。
immutability-helperを設置してみよう。

```sh
yarn add immutability-helper
```

使い方はこんな感じ

```javascript
import update from 'immutability-helper';

const state1 = ['x'];
const state2 = update(state1, {$push: ['y']}); // ['x', 'y']
```

state1とstate2は別のオブジェクトだ。
updateは対象objectのコピーを作ってそのコピーを更新する。
state2はstate1をコピーして作っているが、別のobjectなので、state2に変更があってもstate1には影響を及ぼさない。
immutability-helperの有効なコマンドを整理した。
githubにあるサンプルコードをそのまま持ってきた。
見ればわかると思うが、arrayとほぼ同じだ。
すこしややこしいところは補足したので、参考にしよう。

* {$push: array}

```javascript
const initialArray = [1, 2, 3];
const newArray = update(initialArray, {$push: [4]}); // => [1, 2, 3, 4]
```
* {$unshift: array}

```javascript
const initialArray = [1, 2, 3];
const newArray = update(initialArray, {$unshift: [0]}); // => [0, 1, 2, 3]
```

* {$splice: array of arrays}

```javascript
const collection = [1, 2, {a: [12, 17, 15]}];
const newCollection = update(collection, {2: {a: {$splice: [[1, 1, 13, 14]]}}});
// => [1, 2, {a: [12, 13, 14, 15]}]
```

\:bulb: 補足
collectionのindexが2、keyがaでitemのスタートindexが1、そこから1個を消して、13と14を追加する。

* {\$set: any}、{\$apply: function}

```javascript
const obj = {a: 5, b: 3};
const newObj = update(obj, {b: {$apply: function(x) {return x * 2;}}});
// => {a: 5, b: 6}
// This is equivalent, but gets verbose for deeply nested collections:
const newObj2 = update(obj, {b: {$set: obj.b * 2}});
```

\:bulb: 補足
結果としてnewObjとnewObj2は同じだ。値としてapplyは関数を、setは式になっている。

* {$merge: object}

```javascript
const obj = {a: 5, b: 3};
const newObj = update(obj, {$merge: {b: 6, c: 7}}); // => {a: 5, b: 6, c: 7}
```

```diff:src/components/KanbanBoardContainer.js
diff --git a/src/components/KanbanBoardContainer.js b/src/components/KanbanBoardContainer.js
new file mode 100644
index 0000000..3197efd
--- /dev/null
+++ b/src/components/KanbanBoardContainer.js
@@ -0,0 +1,142 @@
+import React, { Component } from 'react';
+import update from 'immutability-helper';
+import 'whatwg-fetch';
+import KanbanBoard from './KanbanBoard';
+
+const API_URL = 'http://kanbanapi.pro-react.com';
+const API_HEADERS = {
+  'Content-Type': 'application/json',
+  Authorization: 'CHANGE THIS VALUE',
+};
+
+class KanbanBoardContainer extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      cards: [],
+    };
+    this.addTask = this.addTask.bind(this);
+    this.deleteTask = this.deleteTask.bind(this);
+    this.toggleTask = this.toggleTask.bind(this);
+  }
+
+  componentDidMount() {
+    fetch(`${API_URL}/cards`, { headers: API_HEADERS })
+      .then(response => response.json())
+      .then(responseData => this.setState({ cards: responseData }))
+      .catch(error => console.error('Error fetching and parsing data', error));
+  }
+
+  addTask(cardId, taskName) {
+    const prevState = this.state;
+    const cardIndex = this.state.cards.findIndex(card => card.id === cardId);
+    const newTask = { id: Date.now(), name: taskName, done: false };
+    const nextState = update(this.state.cards, {
+      [cardIndex]: {
+        tasks: { $push: [newTask] },
+      },
+    });
+
+    this.setState({ cards: nextState });
+
+    fetch(`${API_URL}/cards/${cardId}/tasks`, {
+      method: 'post',
+      headers: API_HEADERS,
+      body: JSON.stringify(newTask),
+    })
+      .then((response) => {
+        if (response.ok) {
+          return response.json();
+        }
+        throw new Error("Server response wasn't OK");
+      })
+      .then((responseData) => {
+        newTask.id = responseData.id;
+        this.setState({ cards: nextState });
+      })
+      .catch(() => {
+        this.setState(prevState);
+      });
+  }
+
+  deleteTask(cardId, taskId, taskIndex) {
+    const cardIndex = this.state.cards.findIndex(card => card.id === cardId);
+    const prevState = this.state;
+    const nextState = update(this.state.cards, {
+      [cardIndex]: {
+        tasks: { $splice: [[taskIndex, 1]] },
+      },
+    });
+
+    this.setState({ cards: nextState });
+
+    fetch(`${API_URL}/cards/${cardId}/tasks/${taskId}`, {
+      method: 'delete',
+      headers: API_HEADERS,
+    })
+      .then((response) => {
+        if (!response.ok) {
+          throw new Error("Server response wasn't OK");
+        }
+      })
+      .catch((error) => {
+        console.error('Fetch error:', error);
+        this.setState(prevState);
+      });
+  }
+
+  toggleTask(cardId, taskId, taskIndex) {
+    const prevState = this.state;
+    const cardIndex = this.state.cards.findIndex(card => card.id === cardId);
+    let newDoneValue;
+    const nextState = update(
+      this.state.cards,
+      {
+        [cardIndex]: {
+          tasks: {
+            [taskIndex]: {
+              done: {
+                $apply: (done) => {
+                  newDoneValue = !done;
+                  return newDoneValue;
+                },
+              },
+            },
+          },
+        },
+      },
+    );
+
+    this.setState({ cards: nextState });
+
+    fetch(`${API_URL}/cards/${cardId}/tasks/${taskId}`, {
+      method: 'put',
+      headers: API_HEADERS,
+      body: JSON.stringify({ done: newDoneValue }),
+    })
+      .then((response) => {
+        if (!response.ok) {
+          throw new Error('Server response wasn\'t OK');
+        }
+      })
+      .catch((error) => {
+        console.error('Fetch error:', error);
+        this.setState(prevState);
+      });
+  }
+
+  render() {
+    return (
+      <KanbanBoard
+        cards={this.state.cards}
+        taskCallbacks={{
+          toggle: this.toggleTask,
+          delete: this.deleteTask,
+          add: this.addTask,
+        }}
+      />
+    );
+  }
+}
+
+export default KanbanBoardContainer;
```

ソースコードが長く見えるが、外部APIと通信しながら、タスクを追加、削除、トグルするため関連関数を追加しただけだ。
componentDidMountはコンポーネントがマウント(レンダーリング)された直後に必ず1回呼び出される関数だ。
Promiseオブジェクトについては割愛する。
APIサーバーとのやりとりを行う際は、fetch~then~catch順でするものだと覚えておく。
使ううちにわかるようになる。

## react/forbid-prop-types

PropTypesにてany、array、objectはeslintで使用が禁止されている。
確かに、どんなタイプなのか明らかにしないとPropTypesを使う意味がない。
arrayの代わりにarrayOfを、objectの代わりにshapeを使おう。
詳細は、[PropTypesのValidator](http://qiita.com/park-jh/items/a7dc06a478b944e59222)で確認しよう。

```diff:src/components/KanbanBoard.js
diff --git a/src/components/KanbanBoard.js b/src/components/KanbanBoard.js
index b89409d..8ee231e 100644
--- a/src/components/KanbanBoard.js
+++ b/src/components/KanbanBoard.js
@@ -9,6 +9,7 @@ class KanbanBoard extends Component {
         <List
           id="todo"
           title="To Do"
+          taskCallbacks={this.props.taskCallbacks}
           cards={
             this.props.cards.filter(card => card.status === 'todo')
           }
@@ -16,6 +17,7 @@ class KanbanBoard extends Component {
         <List
           id="in-progress"
           title="In Progress"
+          taskCallbacks={this.props.taskCallbacks}
           cards={
             this.props.cards.filter(card => card.status === 'in-progress')
           }
@@ -23,6 +25,7 @@ class KanbanBoard extends Component {
         <List
           id="done"
           title="Done"
+          taskCallbacks={this.props.taskCallbacks}
           cards={
             this.props.cards.filter(card => card.status === 'done')
           }
@@ -34,6 +37,11 @@ class KanbanBoard extends Component {

 KanbanBoard.propTypes = {
   cards: PropTypes.arrayOf(PropTypes.object).isRequired,
+  taskCallbacks: PropTypes.shape({
+    toggle: PropTypes.func,
+    delete: PropTypes.func,
+    add: PropTypes.func,
+  }).isRequired,
 };

 export default KanbanBoard;
```

```diff:src/components/List.js
diff --git a/src/components/List.js b/src/components/List.js
index c548d80..2ba25ee 100644
--- a/src/components/List.js
+++ b/src/components/List.js
@@ -7,6 +7,7 @@ class List extends Component {
     const cards = this.props.cards.map(card =>
       (<Card
         key={card.id}
+        taskCallbacks={this.props.taskCallbacks}
         id={card.id}
         title={card.title}
         description={card.description}
@@ -27,6 +28,11 @@ class List extends Component {
 List.propTypes = {
   title: PropTypes.string.isRequired,
   cards: PropTypes.arrayOf(PropTypes.object).isRequired,
+  taskCallbacks: PropTypes.shape({
+    toggle: PropTypes.func,
+    delete: PropTypes.func,
+    add: PropTypes.func,
+  }).isRequired,
 };

 export default List;
```

```diff:src/components/Card.js
diff --git a/src/components/Card.js b/src/components/Card.js
index 5025f5f..19e0f4c 100644
--- a/src/components/Card.js
+++ b/src/components/Card.js
@@ -22,7 +22,11 @@ class Card extends Component {
       cardDetails = (
         <div className="card__details">
           <span dangerouslySetInnerHTML={{ __html: Marked(this.props.description) }} />
-          <CheckList cardId={this.props.id} tasks={this.props.tasks} />
+          <CheckList
+            cardId={this.props.id}
+            tasks={this.props.tasks}
+            taskCallbacks={this.props.taskCallbacks}
+          />
         </div>
       );
     }
@@ -81,6 +85,11 @@ Card.propTypes = {
   description: PropTypes.string.isRequired,
   color: PropTypes.string.isRequired,
   tasks: PropTypes.arrayOf(PropTypes.object).isRequired,
+  taskCallbacks: PropTypes.shape({
+    toggle: PropTypes.func,
+    delete: PropTypes.func,
+    add: PropTypes.func,
+  }).isRequired,
 };

 export default Card;
```

```diff:src/components/CheckList.js
diff --git a/src/components/CheckList.js b/src/components/CheckList.js
index 4de883f..bd1b701 100644
--- a/src/components/CheckList.js
+++ b/src/components/CheckList.js
@@ -2,12 +2,37 @@ import React, { Component } from 'react';
 import PropTypes from 'prop-types';

 class CheckList extends Component {
+  constructor(props) {
+    super(props);
+    this.checkInputKeyPress = this.checkInputKeyPress.bind(this);
+  }
+
+  checkInputKeyPress(evt) {
+    const event = evt;
+    if (event.key === 'Enter') {
+      this.props.taskCallbacks.add(this.props.cardId, event.target.value);
+      event.target.value = '';
+    }
+  }
+
   render() {
-    const tasks = this.props.tasks.map(task => (
+    const tasks = this.props.tasks.map((task, taskIndex) => (
       <li key={task.id} className="checklist__task">
-        <input type="checkbox" defaultChecked={task.done} />
+        <input
+          type="checkbox"
+          checked={task.done}
+          onChange={
+            this.props.taskCallbacks.toggle.bind(null, this.props.cardId, task.id, taskIndex)
+          }
+        />
         {task.name}{' '}
-        <span className="checklist__task--remove" />
+        <span
+          className="checklist__task--remove"
+          role="presentation"
+          onClick={
+            this.props.taskCallbacks.delete.bind(null, this.props.cardId, task.id, taskIndex)
+          }
+        />
       </li>
     ));

@@ -18,6 +43,7 @@ class CheckList extends Component {
           type="text"
           className="checklist--add-task"
           placeholder="Type then hit Enter to add a task"
+          onKeyPress={this.checkInputKeyPress}
         />
       </div>
     );
@@ -25,7 +51,13 @@ class CheckList extends Component {
 }

 CheckList.propTypes = {
+  cardId: PropTypes.number.isRequired,
   tasks: PropTypes.arrayOf(PropTypes.object).isRequired,
+  taskCallbacks: PropTypes.shape({
+    toggle: PropTypes.func,
+    delete: PropTypes.func,
+    add: PropTypes.func,
+  }).isRequired,
 };

 export default CheckList;
```

eslintから”jsx内でbindを使うな！”と怒られる。
今までconstructorでbindすることで回避していたが、ここではarrow functionを使ってみよう。
__arrow functionでのthisはarrow functionが宣言された場所を指しているため、bindが要らない。__

```diff:src/components/CheckList.js
diff --git a/src/components/CheckList.js b/src/components/CheckList.js
index bd1b701..d554084 100644
--- a/src/components/CheckList.js
+++ b/src/components/CheckList.js
@@ -22,7 +22,7 @@ class CheckList extends Component {
           type="checkbox"
           checked={task.done}
           onChange={
-            this.props.taskCallbacks.toggle.bind(null, this.props.cardId, task.id, taskIndex)
+            () => this.props.taskCallbacks.toggle(this.props.cardId, task.id, taskIndex)
           }
         />
         {task.name}{' '}
@@ -30,7 +30,7 @@ class CheckList extends Component {
           className="checklist__task--remove"
           role="presentation"
           onClick={
-            this.props.taskCallbacks.delete.bind(null, this.props.cardId, task.id, taskIndex)
+            () => this.props.taskCallbacks.delete(this.props.cardId, task.id, taskIndex)
           }
         />
       </li>
```

これで、eslintからのエラーは全部クリアした。
著者が学習用で用意したサーバーからデータを取ってくるので、すでに他の人々が入れたデータが表示される。
カードの中のタスクを追加、削除、トグルしてみよう。