Help us understand the problem. What is going on with this article?

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

はじめに

この記事ではJavaScriptのライブラリであるReactとReactの機能であるhookを使用して簡単なToDoアプリの実装を行います。
以前に書いた記事をベースに書き換えるので、そちらも参考にしてみてください。
JavaScriptのUIライブラリ ReactでToDoアプリを作成してみました
Reactのドキュメントチュートリアル(三目並べ)を一通り行った後の練習になるように書きたいと思います。
環境構築に関しては、create-react-appを使用して作成しています。
環境構築に関しては以前書いた記事があります。
ソースコード

目次

  1. コンポーネントの確認
  2. 各コンポーネントの解説
  3. まとめ

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

すぐに動かせる環境を置いておきます。


See the Pen
ReactToDo with Hook
by oq-Yuki-po (@oq-yuki-po)
on CodePen.



ReactはUIのパーツをコンポーネントという独立した一つの部品とみなして構成していきます。
今回の例では下記の様に分割しました。
以前は、これらのコンポーネントを全てクラスで定義して行きましたが今回は関数コンポーネントで定義して行きます。

ReactToDoApp.png

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

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

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

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

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

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

2-1. ToDo.js

2-1-1. ソース全体

ToDo.js
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
import React, { useState, useReducer } from "react";
import TaskAdd from './TaskAdd';
import TaskList from './TaskList';

useStateuseReducerがhookと呼ばれるものです。
これを使用することで、クラスコンポーネントで行なっていた状態管理が関数コンポーネントでも行えます。

2-1-3. ToDoコンポーネント

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を定義していたと思います。
以前の記事では、下記の様に定義していました。

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

関数でコンポーネントを定義する際は以下の様になります。

ToDoコンポーネント(クラス版)
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の定義

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が入っています。
actiondispatchで、この関数を実行するときに指定したパラメータが格納されています。
action.typeで、どの操作なのかを判定するのに使用しています。
adddeleteはそれぞれ、TaskListに新規にタスクを追加、指定したタスクを削除の処理を行なっています。

2-2. TaskAdd.js

2-2-1. ソース全体

TaskAdd.js
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の定義

stateの定義
const [NewTask, setTask] = useState('')
const [TaskId, setTaskId] = useState(props.id)
const [ErrorMessage, setErrorMessage] = useState('')

Task.jsと同じ様に定義しています。
const [状態管理したい変数, 状態を変更する関数] = useState(状態管理したい変数の初期値)

2-2-3. handleClick

TaskAdd.jsの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} />
})

typeNewTaskはactionで取れる様に追加しています。

2-2-4. render

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

Task.js
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の定義

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

Task.js
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

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を使用して、クラスコンポーネントを関数コンポーネントのみで書き換えてみました。
スッキリ書き換えられるのはメリットに感じました。
ドキュメントを読む限りでは、完全に互換性がある訳では無いそうなので更に勉強が必要そうです。。。
最後まで見てくださり、ありがとうございます。
質問や、指摘は大歓迎ですので、よろしくお願いします。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away