はじめに
この記事ではJavaScriptのライブラリであるReactを使用して簡単なToDoアプリの実装を行います。
クライアント側のみの実装になります。
Reactのドキュメントやチュートリアル(三目並べ)を一通り行った後の練習になるように書きたいと思います。
環境構築に関しては、create-react-app
を使用して作成しています。環境構築に関しては以前書いた記事があります。
もちろん、オレオレな環境でもokです。
Reactについては初心者なので認識の齟齬や到らない点も多々あると思いますが、よろしくお願いします。
目次
- 環境準備
- コンポーネントの確認
- ファイル構成
- Reactで保持するデータについて
- 各コンポーネントの解説
- まとめ
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で作成していきたいと思います。
2. コンポーネントの確認
ReactはUIのパーツをコンポーネントという独立した一つの部品とみなして構成していきます。
今回の例では下記の様に分割しました。
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と同じですね。
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コンポーネントを見てみましょう。
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コンポーネント
をレンダリングしています。
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を持たせる様にします。
ここまでを、再度コンポーネントのイメージと一緒に確認すると以下の様になります。
本当は更新した際のステータスもToDoコンポーネントで保持するべきだと思いますが省略。
5. 各コンポーネントの解説
各コンポーネントの関係をイメージにすると以下のようになります。
一つずつ確認しながら、見てみてください
5-1.全体図
5-2. ToDo.js
5-2-1. ソース全体
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 React from "react";
import TaskAdd from './TaskAdd';
import TaskList from './TaskList';
5-2-3. stateの定義と関数の登録
-
タスクのリスト
として TaskList -
タスクの識別子(id)
として TaskId
をstateとして定義しています。
-
deleteTask
はタスクの削除 -
addTask
はタスクの登録
を示しています。
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
でタスクリストを更新します。
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コンポーネント
になっています。
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
の方に削除する関数を持たせているのか疑問に思う方も居ると思いますが
後に解決すると思うので、ここでは送っているということだけ覚えておいてください。
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. ソース全体
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 React from "react";
import Task from './Task'
5-3-3. stateの定義と関数の登録
-
新規タスク名
-
タスクID(ToDoコンポーネントから受け取ったもの)
-
エラーメッセージ
-
handleClick
はAddボタン押下時の関数 -
handleChange
はテキストボックスの変更を検知してstateの新規タスク名を書き換えています
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を書いてた人なら、平常運転的な感じですかね?
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を振ったり、タスクのテキストボックスやエラーメッセージを空白にしているのみです。
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タグを並べているだけです。
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. ソース全体
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はタスクのチェックボックスのイベント処理に使用しています。
constructor(props) {
super(props);
this.state = {
isDone: this.props.isDone,
}
this.handleChange = this.handleChange.bind(this);
}
5-4-3. checkTaskStatus
タスクのラベルにcssのクラスを適応させています。
ちょっと不格好。。
checkTaskStatus(isDone) {
if (isDone) {
return 'isDone task-item-label'
} else {
return 'WorkInProgress task-item-label'
}
5-4-4. handleChange
stateのis_Doneを見て判定しているだけです。
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みたに定義して書かなくていいから楽ですよね。
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を表示しているだけです。
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など関門が立ちはだかっているのが見えますが、地道に取り組んで行きたいと思っています。
また、最後までお読みくださりありがとうございます。
もしかしたら、有識者の方から見たらデタラメな書き方をしているかもしれませんがご容赦ください。
質問、指摘、コメントは大歓迎ですので、よろしくお願いします。