React HookのuseStateを使ってできるだけシンプルなReactのTodoアプリケーションの作成を行います。

Reactビギナーの人であればuseStateを利用した配列へのオブジェクトの追加、削除、更新方法がわからない人も多いと思います。Todoアプリはそれらの処理をすべて含んでいるのでReactビギナーの人にとってアプリケーション作成の基礎を学ぶのに一番良い教材です。…XXXのspread operatorsが頻繁に出てくるのでこの記事を通して使いこなせるように慣れていきましょう。

React useState ToDoアプリ
React useState ToDoアプリ

環境の構築

任意のディレクトリでnpx crate-react-appを実行してReactのプロジェクトの開発環境を構築します。


 $ npx create-react-app my-app

実行が環境したらmy-appディレクトリに移動してnpm startコマンドを実行します。


 $ cd my-app
 $ npm start 

npm startコマンドが完了したら、ブラウザが自動で起動しReactのトップページが表示されれば環境構築は完了です。

ToDoアプリケーションの作成

作成の準備

Reactインストールディレクトリの下にあるsrcディレクトリの下にあるApp.jsファイルを下記のように変更を行います。TodoのアプリケーションのコードはToDoListファイルの中に記述するためToDoListファイルをimportしています。


import TodoList from './components/TodoList';

function App() {
  return (
    <div style={{ margin: "2em"}}>
      <TodoList/>
    </div>
  );
}

export default App;

TodoList.jsファイルの作成

TodoList.jsファイルはsrcディレクトリの下にcomponentsディレクトリを作成してその下に保存します。

作成開始時のToDoList.jsファイルには下記を記述します。


import { useState } from 'react';

const TodoList = () => {

    const initialState = [
        {
            task: 'Learn vue.js',
            isCompleted: false
        },
        {
            task: 'Learn React Hook',
            isCompleted: false
        },
        {
            task: 'Learn Gatsby.js',
            isCompleted: false
        },     
    ]

    const [todos, setTodos] = useState(initialState);

    return (
        <div>
            <h1>ToDo List</h1>
        </div>
  );
}

export default TodoList;

Todoリストの元になる配列initialStateには、3つのタスクがオブジェクトで保存されています。useStateにinitialStateを指定することでtodosにはTodoリストが保存されます。todosの更新にはsetTodosを利用して行います。

ブラウザで確認すると画面にはToDo Listと表示されます。

ToDoリストの開始画面
ToDoリストの開始画面

ToDoリストの表示

ToDoリストを画面上に表示するためにTodoリストが保存されているtodosをmap関数で展開します。


<div>
    <h1>ToDo List</h1>
    <ul>
        { todos.map((todo, index) => (
        <li key={ index }>{ todo.task }</li>
        ))}
    </ul>
</div>

ブラウザで確認を行うとtodosに保存されている3つのタスクがリスト表示されます。

タスクのリスト表示
タスクのリスト表示

inputタグの設定とuseStateの追加

新しいタスクを追加するためのinputタグをリストの上に追加します。


<h1>ToDo List</h1>
Add Task : <input placeholder="Add New Task" />
<ul>
    { todos.map((todo, index) => (
    <li key={ index }>{ todo.task }</li>
    ))}
</ul>

ブラウザで確認すると入力枠が表示されます。

入力枠表示
入力枠表示

入力した情報を保持するための新しい変数taskをuseStateで追加します。


const [task, setTask] = useState('')

inputタグに入力した値を取得するためにonChangeイベントを設定し、value属性の値に追加したtaskを指定します。onChangeイベントで実行する関数名は、handleNewTaskとします。


Add Task : <input value={ task } placeholder="Add New Task" onChange={handleNewTask}/>

onChangeイベントを設定すると文字を入力するたびにイベントが発行されます。

イベント時に実行されるhandleNewTask関数を追加します。inputで入力した値はevent.target.valueで取得することができるので、取得できているかconsole.logを利用して確認します。


const handleNewTask = (event) => {
    console.log(event.target.value)
}

文字を入力する度にコンソールログに入力した文字が表示されれば正常に動作しているので、console.logからsetTaskに変更を行います。


const handleNewTask = (event) => {
    setTask( event.target.value )
}

上記のようにsetTaskに変更すると文字を入力すると入力欄に入力した文字が表示されます。

input欄に入力文字が表示
input欄に入力文字が表示

Formの設定

入力したタスクをtodoリストに保存するためにFormタグを追加する必要があります。FormタグにはonSubmitイベントを追加します。onSubmitイベントで実行する関数名は、handleSubmitとします。ボタンをonSubmitイベントによりEnterを押すとhandelSubmitが実行されます。


<form onSubmit={handleSubmit}>
  Add Task :
  <input
    value={task}
    placeholder="Add New Task"
    onChange={handleNewTask}
  />
  <button type="submit">Add</button>
</form>

タスク追加機能の追加

input要素に入力した文字列をtodosに追加するためにhandleSubmit関数を追加します。


const handleSubmit = (event) => {
    event.preventDefault()
    if(task === '') return
    setTodos(todos => [...todos,{ task, isCompleted: false}])
    setTask('')
}

通常のHTMLのフォームではsubmitを実行するとページのリロードが行われます。event.preventDefault()を設定することで通常の動作を停止させています。

handleSubmit関数の中では、useStateで追加した2つの変数のtaskとtodosさらにSetTodosとSetTaskを利用します。

taskには入力欄で入力した文字列が入っているはずなので文字列に何も値がない場合は処理が終わります。文字列が入っている場合は、setTodosによりtodosリストに新しいタスクを追加します。setTaskでは入力欄に入力した文字はTodoリストに追加されるのでその文字を入力欄から削除しています。

下記の部分が既存のTodoリストに入力欄で入力した値を追加している処理です。spread operatorを利用しています。


todos => [...todos,{ task, isCompleted: false}]

ブラウザ上で入力欄に入力した文字がTodoリストに追加されます。追加する際はEnterボタンを押してください。下記では追加により4つのタスクになっています。空白のままEnterを押しても何も起こりません。

リストに新しいタスクを追加
リストに新しいタスクを追加
Vue.jsなどに慣れている人であれば配列の追加はtodos.push(todo)と記述するつ思うかもしれませんが、配列に新たにオブジェクトを追加する場合ReactのuseStateではpushメソッドは利用しません。

setTodosで配列にオブジェクトを追加する前に…todosや{task, isCompleted:false}がどういった形のものなのかコンソールに表示して確認しておきましょう。


if(task === '') return
    console.log({task,isCompleted:true});
    console.log(...todos);
    setTodos(todos => [...todos,{ task, isCompleted: false}])
    setTask('')
}

下記のような形であることが確認できます。

todosに追加前のタスクオブジェクトとtodos配列
todosに追加前のタスクオブジェクトとtodos配列

タスクの削除機能を追加

タスクの追加が行えたので次はタスクの削除機能を追加します。

liタグにspanタグで囲んでXを追加します。


<li key={ index }>{ todo.task } <span>X</span></li>

タスクの右にXが表示されます。

削除のためにXを追加
削除のためにXを追加

このXをクリックするとタスクが削除されるように設定を行なっていきます。spanタグにonClickイベントを追加し、関数をhandleRemoveTaskとします。


<li key={ index }>{ todo.task } <span onClick={ () => handleRemoveTask(index) }>X</span></li>

handleRemoveTask関数を追加します。handleRemoveTaskでは削除するタスクを識別するためにindexを渡します。


const handleRemoveTask = index => {
    const newTodos = [...todos]
    newTodos.splice(index,1)
    setTodos(newTodos)
}

handleRemoveTaskの中では現在のTodoリストをspread operatorを利用して新しい配列newTodosに保存します。spliceメソッドを利用して配列のindex番目の要素を1つ削除しています。削除後はsetTodosで新しいTodoリストで既存のTodoリストを書き換えています。

設定後、Xをクリックするとタスクが削除することができます。3つのタスクが削除になり2つになっています。すべてのタスクを削除することも可能です。

タスクを削除
タスクを削除

タスクの削除ではfilter関数を利用することもできます。filter関数では削除を行うのではなくindexを持ったtodoを取り除いています。


const handleRemoveTask = index => {
    const newTodos = [...todos].filter((todo,todoIndex) => todoIndex !== index);
    setTodos(newTodos)
}

ここまでの処理でReact HookのuseStateを利用して、タスクを追加、削除するToDoリストが完成しました。完成時の動作は下記の通りです。

React useState ToDoアプリ
React useState ToDoアプリ

タスク更新機能の追加

Xをクリックするとタスク一覧から削除を行っていましたが、削除ではなくタスクの完了か未完了かを表すisCompletedの値を更新します。

handleRemoveTaskをhandleupdateTaskに変更します。


<span onClick={ () => handleUpdateTask(index) }>X</span>

isCompletedがtrueの場合はタスクが完了したのでタスクが完了したことを表すためstyleのtextDecorationLineをline-throughに文字列の上に横棒を設定します。


<ul>
  { todos.map((todo, index) => (
  <li 
    key={ index } 
    style={ todo.isCompleted === true ? {textDecorationLine: 'line-through'}:{}}
  >
    { todo.task } 
    <span onClick={ () => handleUpdateTask(index) }>X</span></li>
  ))
  }
</ul>

最後にhandleUpdateTaskメソッドを追加します。handleUpdateTaskの引数で設定したindexとtodos配列のindexが一致したtodoのisCompletedの値を現在設定されている値を逆の値に設定しています。trueであればfalseになり、falseであればtrueになります。


const handleUpdateTask = index => {
  let newTodos = todos.map((todo,todoIndex) => {
    if(todoIndex  === index){
        todo.isCompleted = !todo.isCompleted
    }
    return todo;
  })
  setTodos(newTodos);
}

Xをクリックすると横棒が表示され、横棒が表示されている状態でクリックすると横棒が非表示となります。

Xをクリックすると横棒表示
Xをクリックすると横棒表示

削除機能と更新機能を同時追加

削除機能と更新機能を別々に実装しましたが新たにチェックボックスを利用して更新機能を削除機能とは別に追加します。チェックボックはinput要素のtypeをcheckboxとしています。onChageイベントで設定しているhandleUpdateTaskは先ほど更新機能の際に作成したものです。


<ul>
  {todos.map((todo, index) => (
    <li
      key={index}
      style={{
        textDecoration: todo.isCompleted ? 'line-through' : 'none',
      }}
    >
      <input
        type="checkbox"
        checked={todo.isCompleted}
        onChange={() => handleUpdateTask(index)}
      />
      {todo.task}
      <span
        onClick={() => handleRemoveTask(index)}
        style={{ cursor: 'pointer' }}
      >
        X
      </span>
    </li>
  ))}
</ul>
更新用のチェックボックスを追加
更新用のチェックボックスを追加

ここまで作成したコード

作成したコード全体は下記となります。


import { useState } from 'react';

const TodoList = () => {
  const initialState = [
    {
      task: 'Learn vue.js',
      isCompleted: false,
    },
    {
      task: 'Learn React Hook',
      isCompleted: false,
    },
    {
      task: 'Learn Gatsby.js',
      isCompleted: false,
    },
  ];

  const [todos, setTodos] = useState(initialState);

  const [task, setTask] = useState('');

  const handleNewTask = (event) => {
    setTask(event.target.value);
  };

  const handleSubmit = (event) => {
    event.preventDefault();
    if (task === '') return;
    setTodos((todos) => [...todos, { task, isCompleted: false }]);
    setTask('');
  };

  const handleRemoveTask = (index) => {
    const newTodos = [...todos];
    newTodos.splice(index, 1);
    setTodos(newTodos);
  };

  const handleUpdateTask = (index) => {
    const newTodos = todos.map((todo, todoIndex) => {
      if (todoIndex === index) {
        todo.isCompleted = !todo.isCompleted;
      }
      return todo;
    });
    setTodos(newTodos);
  };

  return (
    <div>
      <h1>ToDo List</h1>
      <form onSubmit={handleSubmit}>
        Add Task :
        <input
          value={task}
          placeholder="Add New Task"
          onChange={handleNewTask}
        />
      </form>
      <ul>
        {todos.map((todo, index) => (
          <li
            key={index}
            style={{
              textDecoration: todo.isCompleted ? 'line-through' : 'none',
            }}
          >
            <input
              type="checkbox"
              checked={todo.isCompleted}
              onChange={() => handleUpdateTask(index)}
            />
            {todo.task}
            <span
              onClick={() => handleRemoveTask(index)}
              style={{ cursor: 'pointer' }}
            >
              X
            </span>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default TodoList;

コンポーネント化

作成したコードはすべてTodoList.jsファイルに記述されていましたがTodoList.jsファイルからTodo.js, TodList.js, AddTodo.jsファイルの3つのファイルに分割します。

Todo.jsファイルにはtodosの初期値とTodoList.jsとAddTodo.jsファイルをimportしてpropsでtodosとsetTodosを渡します。


import { useState } from 'react';
import AddTodo from './AddTodo';
import TodoList from './TodoList';

const Todo = () => {
  const initialState = [
    {
      task: 'Learn vue.js',
      isCompleted: false,
    },
    {
      task: 'Learn React Hook',
      isCompleted: false,
    },
    {
      task: 'Learn Gatsby.js',
      isCompleted: false,
    },
  ];

  const [todos, setTodos] = useState(initialState);

  return (
    <div>
      <h1>ToDo List</h1>
      <AddTodo setTodos={setTodos} />
      <TodoList todos={todos} setTodos={setTodos} />
    </div>
  );
};

export default Todo;

AddTodo.jsファイルではタスクの追加の処理のみ記述します。


import { useState } from 'react';

const AddTodo = ({ setTodos }) => {
  const [task, setTask] = useState('');

  const handleNewTask = (event) => {
    setTask(event.target.value);
  };

  const handleSubmit = (event) => {
    event.preventDefault();
    if (task === '') return;
    setTodos((todos) => [...todos, { task, isCompleted: false }]);
    setTask('');
  };

  return (
    <form onSubmit={handleSubmit}>
      Add Task :
      <input value={task} placeholder="Add New Task" onChange={handleNewTask} />
    </form>
  );
};

export default AddTodo;

TodoList.jsはpropsのtodosを受け取りtodosを展開して表示し、更新、削除の関数を設定しています。


import React from 'react';

const TodoList = ({ todos, setTodos }) => {
  const handleRemoveTask = (index) => {
    const newTodos = [...todos];
    newTodos.splice(index, 1);
    setTodos(newTodos);
  };

  const handleUpdateTask = (index) => {
    const newTodos = todos.map((todo, todoIndex) => {
      if (todoIndex === index) {
        todo.isCompleted = !todo.isCompleted;
      }
      return todo;
    });
    setTodos(newTodos);
  };

  return (
    <ul>
      {todos.map((todo, index) => (
        <li
          key={index}
          style={{
            textDecoration: todo.isCompleted ? 'line-through' : 'none',
          }}
        >
          <input
            type="checkbox"
            checked={todo.isCompleted}
            onChange={() => handleUpdateTask(index)}
          />
          {todo.task}
          <span
            onClick={() => handleRemoveTask(index)}
            style={{ cursor: 'pointer' }}
          >
            X
          </span>
        </li>
      ))}
    </ul>
  );
};

export default TodoList;

App.jsファイルからはTodoList.jsファイルではTodo.jsファイルをimportします。


import Todo from './componets/Todo';

function App() {
  return (
    <div style={{ margin: '2em' }}>
      <Todo />
    </div>
  );
}

export default App;

1つのTodoList.jsファイルをコンポーネント化して3つのファイルに分割しましたが処理できる内容は変わりません。