はじめに
この記事ではJavaScriptのライブラリであるReactとReactの機能であるhookを使用して簡単なToDoアプリの実装を行います。
以前に書いた記事をベースに書き換えるので、そちらも参考にしてみてください。
JavaScriptのUIライブラリ ReactでToDoアプリを作成してみました
Reactのドキュメントやチュートリアル(三目並べ)を一通り行った後の練習になるように書きたいと思います。
環境構築に関しては、create-react-app
を使用して作成しています。
環境構築に関しては以前書いた記事があります。
ソースコード
目次
- コンポーネントの確認
- 各コンポーネントの解説
- まとめ
1. コンポーネントの確認
すぐに動かせる環境を置いておきます。
See the Pen ReactToDo with Hook by oq-Yuki-po (@oq-yuki-po) on CodePen.
ReactはUIのパーツをコンポーネントという独立した一つの部品とみなして構成していきます。 今回の例では下記の様に分割しました。 以前は、これらのコンポーネントを全てクラスで定義して行きましたが今回は関数コンポーネントで定義して行きます。ToDoアプリケーションを構成するコンポーネントは全部で4つあります。
-
ToDo
ToDoアプリケーションの全体を表します -
TaskAdd
新しいタスクの追加を行います -
TaskList
追加されたタスクをリストにして表示します -
TaskItem
一つのタスクを表します
2. 各コンポーネントの解説
2-1. ToDo.js
2-1-1. ソース全体
import React, { useState, useReducer } from "react";
import TaskAdd from './TaskAdd';
import TaskList from './TaskList';
function reducer(state, action) {
switch (action.type) {
case 'add':
return [
...state,
action.NewTask
];
case 'delete':
let TaskIndex = 0;
for (var i = 0; i < state.length; i++) {
if (state[i].key.toString() === action.TaskId.toString()) {
TaskIndex = i;
}
}
return state.filter((_, index) => index !== TaskIndex);
default:
return state;
}
}
function ToDo() {
const [TaskId] = useState(0)
const [ToDoList, dispatch] = useReducer(reducer, [])
return (
<main className='todo-component'>
<TaskAdd id={TaskId} dispatch={dispatch} TaskList={ToDoList}/>
<TaskList TaskList={ToDoList} />
</main>
);
}
export default ToDo;
2-1-2. モジュールのインポート
import React, { useState, useReducer } from "react";
import TaskAdd from './TaskAdd';
import TaskList from './TaskList';
useState
とuseReducer
がhookと呼ばれるものです。
これを使用することで、クラスコンポーネントで行なっていた状態管理が関数コンポーネントでも行えます。
2-1-3. ToDoコンポーネント
function ToDo() {
const [TaskId] = useState(0)
const [ToDoList, dispatch] = useReducer(reducer, [])
return (
<main className='todo-component'>
<TaskAdd id={TaskId} dispatch={dispatch} TaskList={ToDoList}/>
<TaskList TaskList={ToDoList} />
</main>
);
}
クラスで定義していた際は、constructorを定義していたと思います。
以前の記事では、下記の様に定義していました。
constructor(props) {
super(props);
this.state = {
TaskList: [],
TaskId: 0
};
this.deleteTask = this.deleteTask.bind(this);
this.addTask = this.addTask.bind(this);
}
関数でコンポーネントを定義する際は以下の様になります。
const [TaskId] = useState(0)
const [ToDoList, dispatch] = useReducer(reducer, [])
stateを定義する際にuseState
を使用します。
const [状態管理したい変数, 状態を変更する関数] = useState(状態管理したい変数の初期値)
TaskIdは子コンポーネントに渡して、そちらで管理するので変更する関数を定義していません。
(定義してる場所が、そもそもどうなの?みたいなツッコミはご勘弁を。後々にAPIとの連携を見越した実装だと解釈お願いします。。。)
const [状態管理したい変数, 状態を変更する関数] = useState(初期値)
配列や複雑なロジックをstateに持たせる時は、TaskListの様にuseReducer
を使用します。
const [状態管理したい変数, reducerで定義した関数(dispatch)] = useReducer(reducer, 状態管理したい変数の初期値);
公式の引用
通常、useReducer が useState より好ましいのは、複数の値にまたがる複雑な state ロジックがある場合や、
前の state に基づいて次の state を決める必要がある場合です。また、useReducer を使えば
コールバックの代わりに dispatch を下位コンポーネントに渡せるようになるため、
複数階層にまたがって更新を発生させるようなコンポーネントではパフォーマンスの最適化にもなります。
引用にも書いてある通り、<TaskAdd id={TaskId} dispatch={dispatch} TaskList={ToDoList}/>
dispatchをTaskAdd
に渡しています。
2-1-4. reducerの定義
function reducer(state, action) {
switch (action.type) {
case 'add':
return [
...state,
action.NewTask
];
case 'delete':
let TaskIndex = 0;
for (var i = 0; i < state.length; i++) {
if (state[i].key.toString() === action.TaskId.toString()) {
TaskIndex = i;
}
}
return state.filter((_, index) => index !== TaskIndex);
default:
return state;
}
}
reducerの定義を行います。
state
は状態管理したい変数、今回はTaskListが入っています。
action
はdispatch
で、この関数を実行するときに指定したパラメータが格納されています。
action.type
で、どの操作なのかを判定するのに使用しています。
add
やdelete
はそれぞれ、TaskListに新規にタスクを追加、指定したタスクを削除の処理を行なっています。
2-2. TaskAdd.js
2-2-1. ソース全体
import React, { useState } from "react";
import Task from './Task'
function TaskAdd(props) {
const [NewTask, setTask] = useState('')
const [TaskId, setTaskId] = useState(props.id)
const [ErrorMessage, setErrorMessage] = useState('')
function handleClick() {
if (NewTask === '') {
setErrorMessage('入力が空です。')
return 0
}
for (var i = 0; i < props.TaskList.length; i++) {
if (props.TaskList[i].props.name === NewTask) {
setErrorMessage('タスク名が重複しています。')
return 0
}
}
props.dispatch({
type: 'add',
NewTask: <Task key={TaskId} id={TaskId} name={NewTask} dispatch={props.dispatch} />
})
setTaskId(TaskId + 1)
setTask('')
setErrorMessage('')
}
return (
<section className='task-creator'>
<h2>Task Add</h2>
<input className='task-item-text' type="text" placeholder="Task" value={NewTask}
onChange={(e) => setTask(e.target.value)} />
<button className='task-add-btn' type="button" onClick={handleClick}>
Add
</button>
<p className='error-msg'>{ErrorMessage}</p>
</section>
)
}
export default TaskAdd;
2-2-2. stateの定義
const [NewTask, setTask] = useState('')
const [TaskId, setTaskId] = useState(props.id)
const [ErrorMessage, setErrorMessage] = useState('')
Task.jsと同じ様に定義しています。
const [状態管理したい変数, 状態を変更する関数] = useState(状態管理したい変数の初期値)
2-2-3. handleClick
function handleClick() {
if (NewTask === '') {
setErrorMessage('入力が空です。')
return 0
}
for (var i = 0; i < props.TaskList.length; i++) {
if (props.TaskList[i].props.name === NewTask) {
setErrorMessage('タスク名が重複しています。')
return 0
}
}
props.dispatch({
type: 'add',
NewTask: <Task key={TaskId} id={TaskId} name={NewTask} dispatch={props.dispatch} />
})
setTaskId(TaskId + 1)
setTask('')
setErrorMessage('')
}
定義したstateにアクセスする際はthis.state.NewTask
の様に行なっていましたが
hookでは単純にNewTask
でアクセスできます。(this.stateの呪縛から開放される!!)
stateの更新の際はthis.setState({ ErrorMessage: '入力が空です。' })
ではなく
state定義時の関数をそのまま使用できます。
つまりsetErrorMessage('入力が空です。')
と書けます。
タスクの追加時にはTask.jsからpropsとして受け取ってあるdispatchを使用しています。
props.dispatch({
type: 'add',
NewTask: <Task key={TaskId} id={TaskId} name={NewTask} dispatch={props.dispatch} />
})
type
とNewTask
はactionで取れる様に追加しています。
2-2-4. render
return (
<section className='task-creator'>
<h2>Task Add</h2>
<input className='task-item-text' type="text" placeholder="Task" value={NewTask}
onChange={(e) => setTask(e.target.value)} />
<button className='task-add-btn' type="button" onClick={handleClick}>
Add
</button>
<p className='error-msg'>{ErrorMessage}</p>
</section>
)
this.state
で取得することが無くなったので、少しスッキリしたと思います。
2-3. Task.js
2-3-1. ソース全体
import React, { useState } from "react";
function Task(props) {
const [isDone, setStatus] = useState(props.isDone)
const task_id = 'task-id-' + props.id.toString()
const css_label = 'task-item-label'
const css_isDone = `${css_label} isDone`
const css_Wip = `${css_label} WorkInProgress`
return (
<li className='task-item-row'>
<input id={task_id} className='task-item-checkbox' type='checkbox'
onChange={() => (isDone === true) ? setStatus(false) : setStatus(true)}>
</input>
<label htmlFor={task_id} className={(isDone === true) ? css_isDone : css_Wip}>
{props.name}
</label>
<i className="material-icons icon" onClick={() => props.dispatch({type:'delete', TaskId:props.id})}>
delete
</i>
</li>
);
}
export default Task;
2-3-2. stateの定義
const [isDone, setStatus] = useState(props.isDone)
const task_id = 'task-id-' + props.id.toString()
const css_label = 'task-item-label'
const css_isDone = `${css_label} isDone`
const css_Wip = `${css_label} WorkInProgress`
stateとclassNameの定数を定義しているのみです。
2-3-3. render
return (
<li className='task-item-row'>
<input id={task_id} className='task-item-checkbox' type='checkbox'
onChange={() => (isDone === true) ? setStatus(false) : setStatus(true)}>
</input>
<label htmlFor={task_id} className={(isDone === true) ? css_isDone : css_Wip}>
{props.name}
</label>
<i className="material-icons icon" onClick={() => props.dispatch({type:'delete', TaskId:props.id})}>
delete
</i>
</li>
);
onChange={() => (isDone === true) ? setStatus(false) : setStatus(true)}
で単純なif文を省略しています。
onClick={() => props.dispatch({type:'delete', TaskId:props.id})}
でpropsで受け取ったdispatchを使用してタスクの削除を行なっています。
2-4. TaskList.js
import React from "react";
function TaskList(props) {
return (
<section className='task-list'>
<h2>Task List</h2>
<ul>
{props.TaskList}
</ul>
</section>
);
}
export default TaskList;
特にクラスと関数で違いは無いでしょうか、強いて言えばクラスで書くより短いくらいでしょうか??
3. まとめ
Reactのhookを使用して、クラスコンポーネントを関数コンポーネントのみで書き換えてみました。
スッキリ書き換えられるのはメリットに感じました。
ドキュメントを読む限りでは、完全に互換性がある訳では無いそうなので更に勉強が必要そうです。。。
最後まで見てくださり、ありがとうございます。
質問や、指摘は大歓迎ですので、よろしくお願いします。