3
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-01-06

はじめに

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

目次

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

1. 環境準備

以下からソースコードを引っ張ってきます。
ソースコード
gitでクローンした場合は、ブランチはtodo-app-pure-cssです。(何かダサい名前なのは目を瞑っておいてください。。。)
動作を確認するために、ブラウザ上で確認できる環境も用意しました。

See the Pen React ToDo by oq-Yuki-po (@oq-yuki-po) on CodePen.

### 1-1. DockerImageのビルド
docker build --rm -f "react-tutorial/Dockerfile" -t react-tutorial:latest "react-tutorial"

1-2. DockerContainerの起動

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

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

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

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

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

1-5. ブラウザで確認

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

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

2. コンポーネントの確認

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

ReactToDoApp.png

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

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

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

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

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

3. ファイル構成

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

 フォルダ構成
.
├── 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以下のファイルのみです。

フォルダ構成
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と同じですね。

Header.js
import React from "react";

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

export default Header;

App.scss

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

index.js

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

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

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

App.js

App.jsHeaderコンポーネントToDoコンポーネントをレンダリングしています。

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

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

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

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

5-1.全体図

ReactToDoStateFlow.png

5-2. ToDo.js

5-2-1. ソース全体

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. モジュールのインポート

import
import React from "react";
import TaskAdd from './TaskAdd';
import TaskList from './TaskList';

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

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

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

  • deleteTaskはタスクの削除
  • addTaskはタスクの登録

を示しています。

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でタスクリストを更新します。

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して更新しています。
後ほど、説明しますがnewTaskTaskコンポーネントになっています。

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の方に削除する関数を持たせているのか疑問に思う方も居ると思いますが
後に解決すると思うので、ここでは送っているということだけ覚えておいてください。

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. ソース全体

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コンポーネントをインポートしています。

import
import React from "react";
import Task from './Task'

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

  • 新規タスク名

  • タスクID(ToDoコンポーネントから受け取ったもの)

  • エラーメッセージ

  • handleClickはAddボタン押下時の関数

  • handleChangeはテキストボックスの変更を検知してstateの新規タスク名を書き換えています

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を書いてた人なら、平常運転的な感じですかね?

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を振ったり、タスクのテキストボックスやエラーメッセージを空白にしているのみです。

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タグを並べているだけです。

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. ソース全体

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はタスクのチェックボックスのイベント処理に使用しています。

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

5-4-3. checkTaskStatus

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

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

5-4-4. handleChange

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

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みたに定義して書かなくていいから楽ですよね。

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を表示しているだけです。

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など関門が立ちはだかっているのが見えますが、地道に取り組んで行きたいと思っています。
また、最後までお読みくださりありがとうございます。
もしかしたら、有識者の方から見たらデタラメな書き方をしているかもしれませんがご容赦ください。
質問、指摘、コメントは大歓迎ですので、よろしくお願いします。

3
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?