JavaScriptのUIライブラリ ReactでToDoアプリを作成してみました

# はじめに
この記事ではJavaScriptのライブラリであるReactを使用して簡単なToDoアプリの実装を行います。
クライアント側のみの実装になります。
Reactの[ドキュメント](https://ja.reactjs.org/docs/getting-started.html)や[チュートリアル](https://ja.reactjs.org/tutorial/tutorial.html)（三目並べ）を一通り行った後の練習になるように書きたいと思います。
環境構築に関しては、`create-react-app`を使用して作成しています。環境構築に関しては以前書いた[記事](https://qiita.com/penpenta/items/2fa98aa885ba8fa7ed90)があります。
もちろん、オレオレな環境でもokです。
Reactについては初心者なので認識の齟齬や到らない点も多々あると思いますが、よろしくお願いします。

## 目次
1. 環境準備
2. コンポーネントの確認
3. ファイル構成
4. Reactで保持するデータについて
5. 各コンポーネントの解説
6. まとめ

## 1. 環境準備
以下からソースコードを引っ張ってきます。
[ソースコード](https://github.com/oq-Yuki-po/react-tutorial/tree/todo-app-pure-css)
gitでクローンした場合は、ブランチは`todo-app-pure-css`です。（何かダサい名前なのは目を瞑っておいてください。。。）
動作を確認するために、[ブラウザ上で確認できる環境](https://codepen.io/oq-yuki-po/pen/gObvWRj)も用意しました。
<p class="codepen" data-height="265" data-theme-id="default" data-default-tab="js,result" data-user="oq-yuki-po" data-slug-hash="gObvWRj" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="React ToDo">
  <span>See the Pen <a href="https://codepen.io/oq-yuki-po/pen/gObvWRj">
  React ToDo</a> by oq-Yuki-po (<a href="https://codepen.io/oq-yuki-po">@oq-yuki-po</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>
### 1-1. DockerImageのビルド

```bash
docker build --rm -f "react-tutorial/Dockerfile" -t react-tutorial:latest "react-tutorial"
```

### 1-2. DockerContainerの起動

```bash
$ docker run --rm -it -v ${PWD}/app:/home/react-tutorial  -p 3000:3000/tcp react-tutorial:latest /bin/bash
root@03887209ce2d:/home# 
```

### 1-3. 追加のパッケージをインストール(コンテナの内部で操作してます)

```bash
root@03887209ce2d:/home# cd react-tutorial
root@03887209ce2d:/home/react-tutorial# yarn install
```

### 1-4. Reactアプリケーションの起動

```bash
root@03887209ce2d:/home/react-tutorial# yarn start
```

### 1-5. ブラウザで確認

Reactアプリケーションの起動に成功すると、以下のような表示がされます。

```bash
Compiled successfully!

You can now view react-tutorial in the browser.

  Local:            http://localhost:3000/
  On Your Network:  http://172.17.0.3:3000/

Note that the development build is not optimized.
To create a production build, use yarn build.
```

ブラウザで`http://localhost:3000/`を入力して開いてみましょう。
そうすると、下記の様な画面が表示されるはずです。
今回は、この画面をReactで作成していきたいと思います。

![SampleReactToDoApp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/190554/6f5a4444-02eb-de11-f5aa-45aa36b58f46.png)

## 2. コンポーネントの確認
ReactはUIのパーツをコンポーネントという独立した一つの部品とみなして構成していきます。
今回の例では下記の様に分割しました。

![ReactToDoApp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/190554/895b5cc4-a1d9-2403-8daf-16de0d763e06.png)

ToDoアプリケーションを構成するコンポーネントは全部で4つあります。

* ToDo
ToDoアプリケーションの全体を表します

* TaskAdd
新しいタスクの追加を行います

* TaskList
追加されたタスクをリストにして表示します

* TaskItem
一つのタスクを表します

## 3. ファイル構成
ファイル構成を確認しましょう。

```bash: フォルダ構成
.
├── Dockerfile
├── README.md
└── app
    ├── README.md
    ├── package.json
    ├── public
    │   ├── favicon.ico
    │   ├── index.html
    │   ├── logo192.png
    │   ├── logo512.png
    │   ├── manifest.json
    │   └── robots.txt
    ├── src
    │   ├── App.js
    │   ├── App.scss
    │   ├── components
    │   │   ├── Header.js
    │   │   ├── Task.js
    │   │   ├── TaskAdd.js
    │   │   ├── TaskList.js
    │   │   └── ToDo.js
    │   └── index.js
    ├── yarn-error.log
    └── yarn.lock
```

沢山のファイルがありますが、今回の記事で注目するのは`app/src`以下のファイルのみです。

```bash:フォルダ構成
src
├── App.js
├── App.scss
├── components
│   ├── Header.js
│   ├── Task.js
│   ├── TaskAdd.js
│   ├── TaskList.js
│   └── ToDo.js
└── index.js
```

### ファイルの解説（ToDo Appのコンポーネントを除く）

`components`配下のファイルが先ほど確認した各コンポーネントに対応しています。
後ほど、詳しく見ていきます。

#### Header.js
`Header.js`はアプリのタイトルの`ToDo`を表しているのみです。
公式ドキュメントで出てくるHelloWorldと同じですね。

```javascript:Header.js
import React from "react";

const Header = () => {
  return (
    <header>
      <h1>ToDo</h1>
    </header>
  );
};

export default Header;
```

#### App.scss
`App.scss`はレイアウトの部分を書いてあります。
デフォルトの状態では見にくいので、見やすい様に少しレイアウトを調整しています。
今回の内容とはあまり関係無いので割愛します。

#### index.js
`index.js`は`Appコンポーネント`をレンダリンしています。
では、Appコンポーネントを見てみましょう。

```javascript:index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));
```

#### App.js
`App.js`は`Headerコンポーネント`と`ToDoコンポーネント`をレンダリングしています。

```javascript:App.js
import React from 'react';
import 'reset-css'
import Header from './components/Header';
import ToDo from './components/ToDo'
import './App.scss';

function App() {
  return (
    <div className="App">
      <Header />
      <ToDo />
    </div>

  );
}

export default App;
```

## 4. Reactで保持するデータについて
各コンポーネントの説明に入る前にReactで保持して、各コンポーネントにどんなデータを渡すのかを説明します。
今回のToDoアプリはタスクの作成、更新、削除ができます。
では、`ToDo Addコンポーネント`と`Task Listコンポーネント`で共通で使用したいデータは何でしょうか？？
言い換えると、`ToDoコンポーネント`で持っておいた方が楽なデータは何でしょうか？？
タスクの新規作成時には、既に同じ名前のタスクが登録されていないか確認したいので`タスクのリスト`は`Task Listコンポーネント`で保持するより
`ToDoコンポーネント`に持つ方が、`ToDo Addコンポーネント`が参照しやすく無いでしょうか？？
また、今回はDBなどの保存する機能はありませんが後々のことを考慮するとなれば各タスクの識別子(id)が必要になってくると考えれます。
なので、`ToDoコンポーネント`にはタスクのIDを持たせる様にします。
ここまでを、再度コンポーネントのイメージと一緒に確認すると以下の様になります。

![ReactToDoApp-state (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/190554/60d5633c-5666-cfb7-5e2c-d96cf82acf80.png)

本当は更新した際のステータスもToDoコンポーネントで保持するべきだと思いますが省略。

## 5. 各コンポーネントの解説

各コンポーネントの関係をイメージにすると以下のようになります。
一つずつ確認しながら、見てみてください

### 5-1.全体図
![ReactToDoStateFlow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/190554/7b3b9fd0-bc12-5cc5-3913-0d928d0eb478.png)


### 5-2. ToDo.js

#### 5-2-1. ソース全体

```jsx:ToDo.js
import React from "react";
import TaskAdd from './TaskAdd';
import TaskList from './TaskList';

class ToDo extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      TaskList: [],
      TaskId: 0
    };
    this.deleteTask = this.deleteTask.bind(this);
    this.addTask = this.addTask.bind(this);
  }

  deleteTask(TaskId) {
    var NewTaskList = this.state.TaskList;
    let TaskIndex = 0;
    for (var i = 0; i < NewTaskList.length; i++) {
      if (NewTaskList[i].key.toString() === TaskId.toString()) {
        TaskIndex = i;
      }
    }
    NewTaskList.splice(TaskIndex, 1);
    this.setState({ TaskList: NewTaskList });
  }

  addTask(newTask) {
    let TaskList = this.state.TaskList;
    TaskList.push(newTask);
    this.setState({ TaskList: TaskList });
  }

  render() {

    return (
      <main className='todo-component'>
        <TaskAdd id={this.state.TaskId} addTask={this.addTask} TaskList={this.state.TaskList} deleteTask={this.deleteTask} />
        <TaskList TaskList={this.state.TaskList} />
      </main>
    );
  }
}

export default ToDo; 
```

#### 5-2-2. モジュールのインポート

```javascript:import
import React from "react";
import TaskAdd from './TaskAdd';
import TaskList from './TaskList';
```

#### 5-2-3. stateの定義と関数の登録

- `タスクのリスト`として TaskList
- `タスクの識別子(id)`として TaskId

をstateとして定義しています。

- `deleteTask`はタスクの削除
- `addTask`はタスクの登録

を示しています。

```javascript:ToDoコンポーネントのconstructor
constructor(props) {
  super(props);
  this.state = {
    TaskList: [],
    TaskId: 0
  };
  this.deleteTask = this.deleteTask.bind(this);
  this.addTask = this.addTask.bind(this);
}
```

#### 5-2-4. deleteTask

タスクの削除はタスクIDを元にタスクリストから削除対象のリストのインデックスを探します。
そして、インデックスが見つかればタスクリストに対して`splice`で対象のタスクの削除を実行します。
削除が完了したら、`this.setState`でタスクリストを更新します。

```javascript:ToDoコンポーネントのdeleteTask
deleteTask(TaskId) {
  var NewTaskList = this.state.TaskList;
  let TaskIndex = 0;
  for (var i = 0; i < NewTaskList.length; i++) {
    if (NewTaskList[i].key.toString() === TaskId.toString()) {
      TaskIndex = i;
    }
  }
  NewTaskList.splice(TaskIndex, 1);
  this.setState({ TaskList: NewTaskList });
}
```

#### 5-2-5. addTask

新しいタスクの情報をタスクリストに`push`して更新しています。
後ほど、説明しますが`newTask`は`Taskコンポーネント`になっています。

```javascript:ToDoコンポーネントのaddTask
addTask(newTask) {
  let TaskList = this.state.TaskList;
  TaskList.push(newTask);
  this.setState({ TaskList: TaskList });
}
```

#### 5-2-6. render

`TaskAdd`には、以下の`prop`が送られています。

* TaskId
* TaskList
* `addTask関数`
* `deleteTask関数`

`TaskList`にはタスクリストのみがpropとして送られています。

なぜ`TaskAdd`の方に削除する関数を持たせているのか疑問に思う方も居ると思いますが
後に解決すると思うので、ここでは送っているということだけ覚えておいてください。

```jsx:ToDoコンポーネントのrender
render() {
  return (
    <main className='todo-component'>
      <TaskAdd id={this.state.TaskId} addTask={this.addTask} TaskList={this.state.TaskList} deleteTask={this.deleteTask} />
      <TaskList TaskList={this.state.TaskList} />
    </main>
  );
}
```

### 5-3. TaskAdd.js

#### 5-3-1. ソース全体

```jsx:TaskAdd
import React from "react";
import Task from './Task'

class TaskAdd extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            NewTask: '',
            TaskId: this.props.id,
            ErrorMessage: '',
        };
        this.handleClick = this.handleClick.bind(this);
        this.handleChange = this.handleChange.bind(this);
    }

    handleChange(event) {
        this.setState({ NewTask: event.target.value });
    }
    handleClick() {
        // 空白チェック
        if (this.state.NewTask === '') {
            this.setState({ ErrorMessage: '入力が空です。' })
            return 0
        }
        // 重複チェック
        for (var i = 0; i < this.props.TaskList.length; i++) {
            if (this.props.TaskList[i].props.name === this.state.NewTask) {
                this.setState({ ErrorMessage: 'タスク名が重複しています。' })
                return 0
            }
        }
        let TaskId = this.state.TaskId;
        this.props.addTask(<Task key={TaskId} id={TaskId} name={this.state.NewTask} deleteTask={this.props.deleteTask} />);
        this.setState({ TaskId: TaskId + 1 })
        this.setState({ NewTask: '' })
        this.setState({ ErrorMessage: '' })
    }

    render() {
        return (
            <section className='task-creator'>
                <h2>Task Add</h2>
                <input className='task-item-text' type="text" placeholder="Task" value={this.state.NewTask} onChange={this.handleChange} />
                <button className='task-add-btn' type="button" onClick={this.handleClick}>Add</button>
                <p className='error-msg'>{this.state.ErrorMessage}</p>
            </section>
        );
    }
}

export default TaskAdd; 
```

#### 5-3-2. モジュールのインポート

タスクを新規に作成するので`Taskコンポーネント`をインポートしています。

```javascript:import
import React from "react";
import Task from './Task'
```

#### 5-3-3. stateの定義と関数の登録

- 新規タスク名
- タスクID(ToDoコンポーネントから受け取ったもの)
- エラーメッセージ

- `handleClick`はAddボタン押下時の関数
- `handleChange`はテキストボックスの変更を検知してstateの新規タスク名を書き換えています

```javascript:TaskAddコンポーネントのconstructor
constructor(props) {
    super(props);
    this.state = {
        NewTask: '',
        TaskId: this.props.id,
        ErrorMessage: '',
    };
    this.handleClick = this.handleClick.bind(this);
    this.handleChange = this.handleChange.bind(this);
```

#### 5-3-4. handleChange

シンプルにsetStateで値を書き換えています。
renderの箇所で`onChange` で指定するとできます。
JavaScriptを書いてた人なら、平常運転的な感じですかね？

```javascript:TaskAddコンポーネントのhandleChange
handleChange(event) {
    this.setState({ NewTask: event.target.value });
}
```

#### 5-3-5. handleClick

Addボタン押下時の関数です。
空白チェックや重複チェックは特に言うことは無いと思います。
propsで受け取ったTaskListをここで使用して、重複の確認をしています。
処理の途中で抜けるのに`return 0`にしてるけど、あまりよろしく無いかも。。。。

諸々のチェックが完了すると、`ToDoコンポーネント`からpropsとして受け取った`addTask関数`を使用してリストに新しいタスクを登録しています。
タスク自体は`Task`としてコンポーネント化しているので、addTaskの引数は`Task`になります。

deleteTaskをTaskAddコンポーネントに持たせている理由ですが、タスクIDと削除機能がセットである方が削除処理が書きやすいからです。
そして、削除処理を持たせることができるタイミングがタスクの新規作成時だからです。
何か別の方法も考えられそうですが。。。（状態管理のフレームワークを使わない方向で。。）

最後のsetStateは新しいタスクIDを振ったり、タスクのテキストボックスやエラーメッセージを空白にしているのみです。

```jsx:TaskAddコンポーネントのhandleClick
handleClick() {
    // 空白チェック
    if (this.state.NewTask === '') {
        this.setState({ ErrorMessage: '入力が空です。' })
        return 0
    }
    // 重複チェック
    for (var i = 0; i < this.props.TaskList.length; i++) {
        if (this.props.TaskList[i].props.name === this.state.NewTask) {
            this.setState({ ErrorMessage: 'タスク名が重複しています。' })
            return 0
        }
    }
    let TaskId = this.state.TaskId;
    this.props.addTask(<Task key={TaskId} id={TaskId} name={this.state.NewTask} deleteTask={this.props.deleteTask} />);
    this.setState({ TaskId: TaskId + 1 })
    this.setState({ NewTask: '' })
    this.setState({ ErrorMessage: '' })
}
```

#### 5-3-6. render

特に説明はいらないと思います。普通のhtmlタグを並べているだけです。

```jsx:TaskAddコンポーネントのrender
render() {
    return (
        <section className='task-creator'>
            <h2>Task Add</h2>
            <input className='task-item-text' type="text" placeholder="Task" value={this.state.NewTask} onChange={this.handleChange} />
            <button className='task-add-btn' type="button" onClick={this.handleClick}>Add</button>
            <p className='error-msg'>{this.state.ErrorMessage}</p>
        </section>
    );
```

### 5-4. Task.js

#### 5-4-1. ソース全体

```jsx:Task.js
import React from "react";

class Task extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            isDone: this.props.isDone,
        }
        this.handleChange = this.handleChange.bind(this);
    }

    checkTaskStatus(isDone) {
        if (isDone) {
            return 'isDone task-item-label'
        } else {
            return 'WorkInProgress task-item-label'
        }
    }

    handleChange() {
        if (this.state.isDone) {
            this.setState({ isDone: false })
        } else {
            this.setState({ isDone: true })
        }
    }

    render() {
        return (
            <li className='task-item-row'>
                <input id={'task-id-' + this.props.id.toString()} className='task-item-checkbox' type='checkbox' onChange={this.handleChange}></input>
                <label htmlFor={'task-id-' + this.props.id.toString()} className={this.checkTaskStatus(this.state.isDone)}>{this.props.name}</label>
                <i className="material-icons icon" onClick={() => this.props.deleteTask(this.props.id)}>delete</i>
            </li>
        );
    }
}
export default Task; 
```

#### 5-4-2. stateの定義と関数の登録

stateにはタスクが完了したかどうかを判定するis_Doneフラグ
handleChangeはタスクのチェックボックスのイベント処理に使用しています。

```javascript:Taskコンポーネントのconstructor
constructor(props) {
    super(props);
    this.state = {
        isDone: this.props.isDone,
    }
    this.handleChange = this.handleChange.bind(this);
}
```

#### 5-4-3. checkTaskStatus

タスクのラベルにcssのクラスを適応させています。
ちょっと不格好。。

```javascript:TaskコンポーネントのcheckTaskStatus
checkTaskStatus(isDone) {
    if (isDone) {
        return 'isDone task-item-label'
    } else {
        return 'WorkInProgress task-item-label'
    }
```

#### 5-4-4. handleChange

stateのis_Doneを見て判定しているだけです。

```javascript:TaskコンポーネントのhandleChange
handleChange() {
    if (this.state.isDone) {
        this.setState({ isDone: false })
    } else {
        this.setState({ isDone: true })
    }
}
```

#### 5-4-5. render

普通のhtmlタグにOnChangeやOnClickのイベント処理を追加しているだけです。
`onClick={() => this.props.deleteTask(this.props.id)}`
こう書けば、handleChangeみたに定義して書かなくていいから楽ですよね。

```jsx:Taskコンポーネントのrender
render() {
    return (
        <li className='task-item-row'>
            <input id={'task-id-' + this.props.id.toString()} className='task-item-checkbox' type='checkbox' onChange={this.handleChange}></input>
            <label htmlFor={'task-id-' + this.props.id.toString()} className={this.checkTaskStatus(this.state.isDone)}>{this.props.name}</label>
            <i className="material-icons icon" onClick={() => this.props.deleteTask(this.props.id)}>delete</i>
        </li>
    );
}
```

### 5-5. TaskList.js

#### 5-5-1. ソース全体

renderでToDoコンポーネントから受け取ったTaskListを表示しているだけです。

```jsx:TaskList
import React from "react";

class TaskList extends React.Component {
    render() {
        return (
            <section className='task-list'>
                <h2>Task List</h2>
                <ul>
                    {this.props.TaskList}
                </ul>
            </section>
        );
    }
}

export default TaskList; 
```

## 6. まとめ
今回は結構なボリュームになってしまいましたが、うまく説明できたでしょうか？（ちょっと心配・・・）
ほんの数日前にReactを改めて勉強しなおして、それっぽいものは作れたかなと思っていたりします。
私は公式のドキュメントやチュートリアルだけでは、なかなか手が進まなくて四苦八苦したので
今回の記事作成の過程でReactと少しは仲良くなれた気がします。
まだまだ、HookやReduxなど関門が立ちはだかっているのが見えますが、地道に取り組んで行きたいと思っています。
また、最後までお読みくださりありがとうございます。
もしかしたら、有識者の方から見たらデタラメな書き方をしているかもしれませんがご容赦ください。
質問、指摘、コメントは大歓迎ですので、よろしくお願いします。