4
3

More than 1 year has passed since last update.

Reactを使ってTODOアプリを作成する JavaScriptのみ

Posted at

TODOアプリ

今回からReactを用いてTODOアプリを作成していこうと思います。
ただし、React初心者なので以下の手順で作成していこうと思います。

  1. JavaScriptで作成(サーバー側はJSON Serverを使用しaxiosを用いてデータの取得、更新を行う)
  2. JavaScript → TypeScriptに変換
  3. Material UIを使用してデザインをつける
  4. Java(Spring Boot)を用いてサーバー側を構築
  5. AWS?を使用してデプロイする

今回は、今後作成するポートフォリオの前段階用のアプリなのでひとまず動いてサーバー上に公開できるところを目指します。

バージョン

Node.js v18.15.0
yarn v1.22.19

package.json
{
  "name": "todo-app-arrange",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@emotion/react": "^11.10.6",
    "@emotion/styled": "^11.10.6",
    "@mui/material": "^5.12.1",
    "@testing-library/jest-dom": "^5.16.5",
    "@testing-library/react": "^13.4.0",
    "@testing-library/user-event": "^13.5.0",
    "@types/react-datepicker": "^4.10.0",
    "axios": "^1.3.5",
    "react": "^18.2.0",
    "react-beautiful-dnd": "^13.1.1",
    "react-datepicker": "^4.11.0",
    "react-dom": "^18.2.0",
    "react-form-hook": "^0.0.1",
    "react-hook-form": "^7.43.9",
    "react-modal": "^3.16.1",
    "react-scripts": "5.0.1",
    "ulid": "^2.3.0",
    "web-vitals": "^2.1.4"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "devDependencies": {
    "@types/react-modal": "^3.13.1"
  }
}

成果物

https://www.netlify.com/ を利用してここまで開発したアプリをデプロイしました。
まだ必要最低限の機能しか実装しておらず、側だけしか作っておらず押しても何の処理も実行されない部分もあります。

コード

今回のツリー構成は以下のようになっています。
以下のファイル以外にもモックサーバーのdb.jsonを作成しています。

src
 |-apis                         ← モックサーバーと通信するファイルを格納するディレクトリ
 |  |-todos.js                  ← サーバーとの通信用のファイル(CRUD)
 |-components                   ← コンポーネントを格納するディレクトリ
 |     |-App.js                 ← コンポーネントをまとめるファイル
 |     |-TodoAdd.js             ← TODOを新規追加するコンポーネント
 |     |-TodoAddCheckItem.js    ← チェックリスト単体用のコンポーネント
 |     |-TodoAddCheckList.js    ← チェックリストをまとめるコンポーネント
 |     |-TodoItem.js            ← TODO単体用のコンポーネント
 |     |-TodoList.js            ← TODOをリスト化するコンポーネント
 |     |-TodoTitle.js           ← タイトル用のコンポーネント
 |-hooks                        ← カウタムフックを格納するディレクトリ
 |  |-useTodo.js                ← TODOの状態を管理するカスタムフック(todos.jsの具体的な実装部分)
 |-index.js                     ← TODOアプリのトップルート

Ajaxとaxios

各コードの解説に入る前に今回モックサーバーとの通信にはAjax通信を用いました。

Ajax(Asynchrous JavaScript and XML)とはJavaScriptとXML(現在はJSONが主流)を使用して、非同期にクライアントとサーバーとの通信を行う手法です。
非同期通信によりサーバーにリクエストしてレスポンスを待つ間に他の作業ができることから、レスポンスを待っている間もページの処理が止まることはありません。

Ajax通信の大まかな流れは以下の通りです。
1. クライアント(Webブラウザ)から更新に必要なデータをサーバーに送信
2. サーバーは受け取ったデータを整形してクライアントに返す
3. クライアントはサーバーから返された整形済みのデータのJSONを受け取り結果をDOMに反映

このAjaxの仕組みを支えているのがXMLHttpRequestです。XMLHttpRequestとは、クライアントとサーバー間でデータをやり取りするための機能をクライアント側で提供するAPIです。

Ajax通信を行うための便利なNode.jsのライブラリがaxiosです。axiosは非同期処理をよりシンプルに扱うことのできるPromiseをベースとしたHTTP通信を行うためのライブラリで、GETPOSTなどのHTTPリクエストを利用してサーバーの通信、データの取得や交換を行います。

まとめると、Ajaxと呼ばれる非同期通信を行うための手段としてaxiosがあります。

todos.js

サーバーとの通信用のファイルtodos.jsを作成します。
機能は追加取得更新削除の4つです

todos.js
// 作成したモックサーバーとの通信にaxiosを利用する
import axios from "axios";

// ローカルに準備したモックサーバーのURL
const dataUrl = "http://localhost:3100/todos";

/* 
##############################
サーバー上の全TODO取得処理
############################## 
*/
// axios.get()でGETリクエストを送信
// サーバー上の全てのTODO(todos)を取得する関数getAllTodosDataを宣言する
// 他ファイルでgetAllTodosData()を利用できるようにするためexportする
export const getAllTodosData = async () => {

    // 引数に指定したURL(http://localhost:3100/todos)へGETリクエストを送り、
    // 戻される値は全てresponseに保存される
    const response = await axios.get(dataUrl);

    // 通信後、response.dataでレスポンスデータを返す
    return response.data;
};

/* 
##############################
TODO追加処理
############################## 
*/
// axios.post()で新規TODOを追加する
// TODOを追加する関数addTodoDataを宣言する
// 他ファイルでaddTodoData()を利用できるようにするためexportする
// 追加したいデータを引数にとる
export const addTodoData = async (todo) => {

    // 第2引数に、送信したいデータを指定してPOST送信する
    // サーバーに転送することで新規にデータを追加する
    const response = await axios.post(dataUrl, todo);

    // 通信後、response.dataでレスポンスデータを返す
    return response.data
};

/* 
##############################
TODO削除処理
############################## 
*/
// axios.delete()で一致したidのTODOを削除する
// TODOを削除する関数deleteTodoDataを宣言する
// 他ファイルでdeleteTodoData()を利用できるようにするためexportする
export const deleteTodoData = async (id) => {

    await axios.delete(`${dataUrl}/${id}`);

    // 通信後、削除したTODOのidを返す
    return id;
};

/* 
##############################
TODO更新処理
############################## 
*/
// axios.put()で一致したidのTODOを更新する
// TODOを更新する関数updateTodoDataを宣言する
// 他ファイルでupdateTodoData()を利用できるようにするためexportする
export const updateTodoData = async (id, todoItem) => {

    // 第2引数に更新したいデータを渡す
    const response = await axios.put(`${dataUrl}/${id}`, todoItem);

    // 通信後、response.dataでレスポンスデータを返す
    return response.data;
};

useTodo

次にモックサーバーと通信して取得したデータを利用して、画面上で追加取得更新削除できる関数useTodo()カスタムフックを作成します。

useTodo
import {useState, useEffect} from "react";

// 一意なidを生成するulidをインポートする
import {ulid} from "ulid";

// src/apis/todos.js内で宣言してexportした関数をimportする
// getAllTodosData, addTodoData, deleteTodoData, updateTodoDataを
// todoDataオブジェクトとしてまとめてimportする
import * as todoData from "../apis/todos";

// useTodo()カスタムフックを外部で使用できるようexportする
export const useTodo = () => {

    // todoListは現在のTODOの状態、setTodoListは現在のtodoListの状態を更新する関数
    const [todoList, setTodoList] = useState([]);

    /* 
    ##############################
    TODO取得処理
    ############################## 
    */
    // データの取得処理はuseEffectを利用してコンポーネントのマウント後に実装する
    // useEffect()の第2引数には空の依存配列[]を設定しているため、コンポーネントの初回レンダリング時のみ実行
    useEffect(()=> {
        
        // モックサーバーからTODOデータを取得するgetAllTodoData()を実行する
        // モックサーバーからレスポンスデータの取得に成功した場合、then()以降の処理を実行する
        // 引数todoにはモックサーバーから送り返されたresponse.dataが設定される
        todoData.getAllTodosData().then((todo) => {
            
            // モックサーバーからTODOデータを取得後、取得したTODOデータを反転させ、上から順に表示
            // todoListの状態(state)を更新する
            setTodoList([...todo].reverse());
        });
    }, []);

    /* 
    ##############################
    完了/未完了変更処理
    ############################## 
    */
    const toggleTodoItemStatus = (id, done) => {

        // find()メソッドを利用し一致するTODOを取得する
        // done(完了/未完了)の状態を反転させたいTODOをTODOリストから見つける
        const changeTodoItem = todoList.find((todoItem) => todoItem.id === id);

        // 対象のTODOの完了/未完了を反転させる
        const newTodoItem = {...changeTodoItem, done: !done };

        // updataTodoData()を利用して対象のidを持つTODOを更新したら、todoListの状態を更新する
        // モックサーバーからレスポンスデータの取得に成功した場合、then()以降の処理を実行する
        // 引数updatedTodoItemにはモックサーバーから送り返された対象のidを持つTODOが設定される
        todoData.updateTodoData(id, newTodoItem).then((updatedTodoItem) => {

            // TODOリストからTODOをmap()メソッドを利用してひとつ1つ処理する
            const newTodoList = todoList.map((todoItem) => 

                // idが異なる場合、todoListから取り出したtodoItemをそのまま返す
                // idが同じ場合、done(完了/未完了)の状態を反転させたupdatedTodoを返し、
                // 新しい配列newTodoListを作成する
                todoItem.id !== updatedTodoItem.id ? todoItem : updatedTodoItem
            );

            // todoListの現在の状態(state)をnewTodoListの内容に更新
            setTodoList(newTodoList);
        }); 
    }

    /* 
    ##############################
    TODO編集処理
    ############################## 
    */
    const changeTodoItem = (id, title, memo, checkList, priority, difficulty, deadLine) => {

        // 編集するTODOをidを用いてTODOリストから検索する
        const changeTodoItem = todoList.find((todoItem) => todoItem.id === id);
        
        // 対象のTODOの内容を変更する
        const newTodoItem = {...changeTodoItem, 
            title: title,           // titleにタイトルをセット
            memo: memo,             // memoにメモをセット
            checkList: checkList,   // checkListにチェックリストをセット
            priority: priority,     // priotiryに重要度をセット
            difficulty: difficulty, // difficultyに難易度をセット
            deadLine: deadLine,     // deadLineに期限をセット
            createDate: new Date()  // createDateに作成日時をセット
        };

        // updataTodoData()を利用して対象のidを持つTODOを更新する
        todoData.updateTodoData(id, newTodoItem).then((updatedTodoItem) => {

            // idが異なる場合、todoListから取り出したtodoItemをそのまま返す
            // idが同じ場合、TODOの内容を更新したupdatedTodoItemを返し、
            // 新しい配列newTodoListを作成する
            const newTodoList = todoList.map((todoItem) => 
                todoItem.id !== updatedTodoItem.id ? todoItem : updatedTodoItem
            );

            // 最新のtodoListに内容を更新する
            setTodoList(newTodoList);
        });
    }

    /* 
    ##############################
    新規TODO取得処理
    ############################## 
    */
    const addTodoItem = (title, memo, checkList, priority, difficulty, deadLine) => {
        const newTodoItem = {
            id: ulid(),             // idにulidで生成された一意な値をセット
            title: title,           // titleにタイトルをセット
            memo: memo,             // memoにメモをセット
            checkList: checkList,   // checkListにチェックリストをセット
            done: false,            // doneに完了/未完了をセット(初期値は未完了(false))
            priority: priority,     // priotiryに重要度をセット
            difficulty: difficulty, // difficultyに難易度をセット
            deadLine: deadLine,     // deadLineに期限をセット
            createDate: new Date()  // createDateに作成日時をセット
        };

        // addTodoData()を利用して新規TODOを追加する
        // 引数addTodoItemにはモックサーバーから送り返された追加されたTODOが設定される
        todoData.addTodoData(newTodoItem).then((addTodo) => {

            // todoListにaddTodoItemが追加された状態に更新する
            setTodoList([addTodoItem, ...todoList]);
        });
    };

    /* 
    ##############################
    TODO削除処理
    ############################## 
    */
    const deleteTodoItem = (id) => {

        // deleteTodoData()を利用して指定されたidのTODOを削除する
        // deleteTodoData()は一致したidのTODOを削除する関数
        todoData.deleteTodoData(id).then((deleteItemId) => {

            // 削除したTODOとidが一致しないTODOのみの新しいリストを返す
            const newTodoList = todoList.filter((todoItem) => 
                todoItem.id !== deleteItemId
            );

            // todoListの状態を更新する
            setTodoList(newTodoList);
        });
    };

    // 作成した関数と、現在のTODOリストの状態変数todoListを返す
    return {todoList, setTodoList, toggleTodoItemStatus, changeTodoItem, addTodoItem, deleteTodoItem};
};

App.js

App.js
// useRefを利用できるようインポートする
import React, { useState } from "react";

// useTodo()カスタムフックをインポートする
import { useTodo } from "../hooks/useTodo";

// TodoTitleコンポーネントをインポートする
import TodoTitle from "./TodoTitle";

// TodoListコンポーネントをインポートする
import TodoList from "./TodoList";

// TodoAddコンポーネントをインポートする
import TodoAdd from "./TodoAdd";

const App = () => {

  // useTodo()カスタムフックで作成したtodoList、addTodoItemを利用する
  const {todoList, setTodoList, addTodoItem, toggleTodoItemStatus, changeTodoItem, deleteTodoItem} = useTodo();

  // 選択されたTODOリストのinputId、idを更新する関数setInputId
  const [inputId, setInputId] = useState("");

  // 現在のタイトルの現在の状態変数inputTitle、inputTitleを更新する関数setInputTitle
  const [inputTitle, setInputTitle] = useState("");

  // 現在のメモの現在の状態変数inputMemo、inputTitleを更新する関数setInputMemo
  const [inputMemo, setInputMemo] = useState("");

  // 現在のチェックリストの状態変数todoList、todoListを変更する関数setTodoList
  const [checkList, setCheckList] = useState([]);

  // 現在の重要度の状態変数priority、priorityを更新する関数setPriority
  const [priority, setPriority] = useState("");

  // 現在の難易度の状態変数difficulty、difficultyを更新する関数setDifficulty
  const [difficulty, setDifficulty] = useState("");

  // 現在の期限の状態変数inputDeadLine、inputDeadLineを更新する関数setInputDeadLine
  const [inputDeadLine, setInputDeadLine] = useState(new Date());

  // モーダルの表示の有無を設定する変数isShowModal、sShowModalを更新する関数setIsShowModal
  const [isShowModal, setIsShowModal] = useState(false);

  // 追加と編集の変更を管理する変数changeFlg、changeFlgを更新する関数setChageFlg
  // False = 追加、True = 編集
  const [changeFlg, setChangeFlg] = useState(false);

  // 漢字変換・予測変換(サジェスト)選択中か否かの判定
  // 変換中か否かの判定を行い、変換を確定させるエンターに反応しないように振り分ける
  // true=変換中、false=変換中ではない
  const [composing, setComposition] = useState(false);

  // チェックリストの現在の個数itemCount、itemCountを変更する関数setItemCountを定義する
  const [itemCount, setItemCount] = useState(0);

  // チェックリストを追加するinputの現在の状態変数inputValue、inputValueを更新する関数setInputValueを定義する
  const [inputValue, setInputValue] = useState("");


  const startComposition = () => setComposition(true);
  const endComposition = () => setComposition(false);

  /* 
  ##############################
  未完了のTODOリストを表示する
  ############################## 
  */
  const imCompletedList = todoList.filter((todoItem) => {
    return !todoItem.done;
  });

  /* 
  ##############################
  完了済みのTODOリストを表示する
  ############################## 
  */
  const completedList = todoList.filter((todoItem) => {
    return todoItem.done;
  });

  /* 
  ##############################
  TODO追加処理
  ############################## 
  */
  const handleAddTodoItem = () => {

    // TODO入力フォームで入力された文字列を新しいTODOに登録する
    addTodoItem(
      inputTitle, 
      inputMemo, 
      checkList, 
      priority, 
      difficulty, 
      inputDeadLine
    );
    closeModal();
  }

  /* 
  ##############################
  TODO編集処理
  ############################## 
  */
  const handleChangeTodoItem = () => {
    
    // 変更されたTODOを登録する
    changeTodoItem(
        inputId,
        inputTitle,
        inputMemo, 
        checkList, 
        priority, 
        difficulty, 
        inputDeadLine
      );
  }

  /* 
  ##############################
  リセット処理
  ############################## 
  */
  const reset = () => {
    setInputTitle("");
    setInputMemo("");
    setCheckList([]);
    setPriority("");
    setDifficulty("");
    setInputDeadLine(new Date());
  }

  /* 
  ##############################
  TODOリストの順番変更処理(実処理)
  ############################## 
  */
  const reorder = (list, startIndex, endIndex) => {

    // Array.from()メソッドは、反復可能オブジェクトや配列風オブジェクトから
    // シャローコピーされた、新しいArrayインスタンスを生成する
    const result = Array.from(list);

    // Array.splice()メソッドは、配列を操作するメソッド
    // 第1引数には操作を開始する配列のインデックス、第1引数のみの場合、指定したインデックス以降を取り除く
    // 第2引数はオプション、第1引数に3、第2引数に1を指定した場合、3番目の要素を配列から取り出す
    // 第3引数はオブション、第3引数に設定した値が配列に追加される
    const [removed] = result.splice(startIndex, 1);
    result.splice(endIndex, 0, removed);
    return result;
  };

  /* 
  ##############################
  モーダルを非表示処理
  ############################## 
  */
  const closeModal = () => {
      setIsShowModal(false);
      setChangeFlg(!changeFlg);
      reset();
  };

  /* 
  ##############################
  TODOリスト簡易追加処理(エンターキーによる操作)
  ############################## 
  */
  const onKeyDown = (e, key) => {

    switch (key) {
      // 変換中でない時にEnterキーでinputを増やす
      case "Enter":
        // input入力中にエンターを押すとデフォルトではsubmit(送信)になるため
        // e.preventDefault();で阻止する
        e.preventDefault();

        // 変換中ならそのまま何の処理も行わない
        if (composing) break;

        // 変換中でないなら、TODOを追加
        addTodoItem(
          inputTitle, 
          inputMemo, 
          checkList, 
          priority, 
          difficulty, 
          inputDeadLine
        );

        // 追加後にフォーカスを外す
        document.getElementById("simpleAddInput").blur();

        // 入力内容をクリアする
        setInputTitle("");
  
        break;
      default:
        break;
    }
  }

  return (
    <>
      {/* 現在は使わないのでコメントアウト中 */}
      {/* <button>新しいパケットの追加</button> */}

      <div>
        <TodoTitle title="TODO" as="h2" />
        <button 
          onClick={() => {
            setIsShowModal(true); 
            setChangeFlg(false);
          }}
        >
          TODOの作成
        </button>

        <div>
          <input 
            type="text"
            id="simpleAddInput" 
            placeholder="TODOの追加"
            value={inputTitle}
            onChange={(e) => setInputTitle(e.target.value)}
            onKeyDown={(e) => onKeyDown(e, e.key, 1)}
            onCompositionStart={startComposition}
            onCompositionEnd={endComposition}
          />
        </div>

        {/* TODOを追加するダイアログを表示する */}
        {!isShowModal ? "" :
          <TodoAdd 
            title={inputTitle}
            memo={inputMemo}
            checkList={checkList}
            deadLine={inputDeadLine}
            priority={priority}
            difficulty={difficulty}
            inputDeadLine={inputDeadLine}
            changeFlg={changeFlg}
            isShowModal={isShowModal}
            composing={composing}
            setInputTitle={setInputTitle}
            setInputMemo={setInputMemo}
            setCheckList={setCheckList}
            setPriority={setPriority}          
            setDifficulty={setDifficulty}
            setInputDeadLine={setInputDeadLine}
            handleAddTodoItem={handleAddTodoItem}
            handleChangeTodoItem={handleChangeTodoItem}
            setIsShowModal={setIsShowModal}
            closeModal={closeModal}
            reorder={reorder}
            startComposition={startComposition}
            endComposition={endComposition}
          />
        }
          <TodoList 
            todoList={imCompletedList}
            isShowModal={isShowModal} 
            toggleTodoItemStatus={toggleTodoItemStatus}
            changeTodoItem={changeTodoItem}
            deleteTodoItem={deleteTodoItem}
            setTodoList={setTodoList}
            setInputId={setInputId}   
            setInputTitle={setInputTitle}
            setInputMemo={setInputMemo}
            setCheckList={setCheckList}
            setPriority={setPriority}
            setDifficulty={setDifficulty}
            setInputDeadLine={setInputDeadLine}
            setIsShowModal={setIsShowModal}
            setChangeFlg={setChangeFlg}
            reorder={reorder}
          />
        </div>
    </>
  )
}

export default App;

リストの順番を変更する仕組み

今回、ドラッグアンドドロップ(表示順変更)する際の処理を以下のように記述しました。


  // list:要素を順番を変更する配列
  // startIndex:移動前の要素のインデックス番号
  // endIndex:移動後の要素のインデックス番号
  const reorder = (list, startIndex, endIndex) => {

    // 引数で受け取ったlistをコピーして新たなリストを作成する
    const result = Array.from(list);

    // 例:list = [a, b, c, d, e], startIndex=2 の場合
    // removed = result.splice(2, 1) = c となる 
    const [removed] = result.splice(startIndex, 1);

    // 例:removed = c, endIndex = 0 の場合
    // result.splice(0, 0, removed) = [c, a, b, d, e] となる
    result.splice(endIndex, 0, removed);
    return result;
  };

処理の流れとしては以下の通りです。

  1. 対象のリストをコピーする
  2. 動かす要素を対象のリストから取り出す
  3. 取り出した要素を順番を変更してリストに挿入する

Add.js

Add.js
// モーダル表示するためにreact-modalをインポートする
import Modal from "react-modal";

// 期限を入力するためのライブラリとしてDatePickerをインポートする
import DatePicker from "react-datepicker";

// DatePickerで使用するカレンダーのCSSをインポートする
import "react-datepicker/dist/react-datepicker.css"

// バリデーションを行うためのreact-hook-formをインポートする
// 現時点では使用しない
import { useForm } from 'react-hook-form';

import TodoCheckList from "./TodoAddCheckList";

// 重要度用の配列priorityItemsを定義する
const priorityItems = [
    {id: 1, value: ""},
    {id: 2, value: ""}
  ]
  
// 難易度用の配列difficultyItemsを定義する
const diffcultyItems = [
    {id: 1, value: ""},
    {id: 2, value: ""},
    {id: 3, value: ""}
] 

// モーダル画面のデザインを設定
const customStyles = {
    
    //モーダルの中身
    content: {
      width: "500px",
      height: "700px",
      top: "0",
      left: "0",
      right: "0",
      bottom: "0",
      margin: "auto",
      border: "none",
      padding: "30px 120px",
      background: "white",
    },

    //モーダルの外側の部分はoverlayを使用する
    overlay: {
      background: "rgba(62, 62, 62, 0.75)"
    }
};

// react-modalを使用するために宣言する必要あり
// 任意のアプリを設定する create-react-appなら#root
Modal.setAppElement("#root");

const TodoAdd = (props) => {
    
    // react-hook-formの初期設定(現状は使用しない)
    // const { register, handleSubmit, watch, formState: { errors } } = useForm();

    return (
        <div>
            {/* モーダル表示したい部分をModalタグで囲む */}
            <Modal

                // モーダルをの表示処理isOpen
                // 表示/非表示はStateのisShowModalで管理する
                isOpen={props.isShowModal}

                // モーダルが表示された後の処理
                // モーダルが表示されている間、背景のスクロールを禁止する
                onAfterOpen={() => document.getElementById("root").style.position = "fixed"}

                // モーダルが非表示になった後の処理
                // モーダルを閉じた後に画面スクロールできるようにする
                onAfterClose={() => document.getElementById("root").style.position = "unset"}

                // ↓を記述するとモーダル画面の外側をクリックした際にモーダルが閉じる
                // onRequestClose={closeModal}
                
                // モーダルの中身/背景のデザインを設定する
                style={customStyles}
            >
                <div>
                    <div>
                        <p>タイトル*</p>
                        <input  
                            type="text" 
                            value={props.title} 
                            onChange={(e) => props.setInputTitle(e.target.value)}
                        />
                    </div>
                    <p>メモ</p><textarea value={props.memo} onChange={(e) => props.setInputMemo(e.target.value)}/>
                    <p>チェックリスト</p>
                        {/* チェックリストはコンポーネント化して別定義する */}
                        <TodoCheckList 
                            checkList={props.checkList} 
                            setCheckList={props.setCheckList}
                            reorder={props.reorder}
                            composing={props.composing}
                            startComposition={props.startComposition}
                            endComposition={props.endComposition}
                        />
                    <div>
                        <p>重要度</p>
                        {/* map()メソッドを使用して重要度用の配列priorityItemsから要素を取り出す */}
                        {priorityItems.map((priorityItem) => (
                            <label key={priorityItem.id}>
                                <input 
                                    type="radio" 
                                    value={priorityItem.value}
                                    onChange={(e) => props.setPriority(e.target.value)}
                                    checked={props.priority === priorityItem.value}
                                />
                                {priorityItem.value}
                            </label>
                        ))}
                    </div>
                    <div>
                        <p>難易度</p>
                        {/* map()メソッドを使用して難易度用の配列difficultyItemsから要素を取り出す */}
                        <select defaultValue={props.difficulty} onChange={(e) => props.setDifficulty(e.target.value)}>
                            {diffcultyItems.map((diffcultyItem) => (
                                <option key={diffcultyItem.id} value={diffcultyItem.value}>
                                    {diffcultyItem.value}
                                </option>
                            ))}
                        </select>
                    </div>
                    <div>
                        <p>期限</p>
                        {/* 期限はDatePickerを使用する */}
                        {/* minDateを設定することで選択できる日付を制限できる */}
                        {/* minDate={new Date()}のように設定すると本日より前の日付は選択できないようになる */}
                        <DatePicker 
                            selected={props.inputDeadLine}
                            onChange={(date) => props.setInputDeadLine(date)}
                            minDate={new Date()}
                        />
                    </div>
                    <div>
                        {/* changeFlgの値により表示するボタンを変更する */}
                        {props.changeFlg ? 
                            <button 
                                type="submit"
                                onClick={() => {
                                    props.handleChangeTodoItem();
                                    props.closeModal();
                                }}
                            >
                                編集する
                            </button> :
                            <button 
                                type="submit"
                                onClick={() => {
                                    props.handleAddTodoItem();
                                    props.closeModal();
                                }}
                            >
                                作成する
                            </button>
                        }
                        <button onClick={props.closeModal}>キャンセル</button>
                    </div>
                </div>
            </Modal>
        </div>
    );
}

export default TodoAdd;

TodoCheckItem.js

const TodoCheckItem = (props) => {

  return (
    <>
      <input type="checkbox"></input>
      {/**/}
      {/**/}
      <input
        type="text"
        value={props.checkItem.checkItem}
        onChange={e => props.updateCheckList(props.index, e)}
      />
      <button onClick={() => props.deleteCheckList(props.index)}>削除</button>
    </>
  );
}

export default TodoCheckItem;

TodoCheckList.js

TodoCheckList.js
import React, { useState } from 'react';

// ドラッグ&ドロップのライブラリreact-beautiful-dndをインポートする
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';

import TodoCheckItem from "./TodoAddCheckItem";

const TodoCheckList = (props) => {

  // チェックリストの現在の個数itemCount
  // itemCountを変更する関数setItemCountを定義する
  const [itemCount, setItemCount] = useState(0);

  // チェックリストを追加するinputの現在の状態変数inputValue
  // inputValueを更新する関数setInputValueを定義する
  const [inputValue, setInputValue] = useState("");

  /* 
  ##############################
  チェックリストのCSS
  ############################## 
  */
  // 引数:isDraggingOver を使用してドラッグ中とそうでない時のCSSを変更することができる
  const getListStyle = (isDraggingOver) => ({
    background: 'white',
    /* isDraggingOverの型は真偽値、true=ドラッグ中、false=ドラッグ中ではない  */
    /* border: isDraggingOver ? 'solid 5px lightgray' : 'solid 5px white', */
    textAlign: 'left',
  });

  /* 
  ##############################
  チェックアイテムのCSS
  ############################## 
  */
  const getItemStyle = (draggableStyle) => ({
    marginBottom: '0.5rem',

    ...draggableStyle
  });

  /* 
  ##############################
  チェックリスト追加処理(エンターキーによる操作)
  ############################## 
  */

  // エンターキーで新たなチェックリストを追加できるようにする
  const onKeyDown = (e, key, number) => {

    switch (key) {
      // 変換中でない時にEnterキーでinputを増やす
      case "Enter":
        // input入力中にエンターを押すとデフォルトではsubmit(送信)になるため
        // e.preventDefault();で阻止する
        e.preventDefault();

        // 変換中ならそのまま何の処理も行わない
        if (props.composing) break;

        // 変換中でないなら、addCheckList()メソッドでチェックリストを追加
        if (number === 0) addCheckList();

        break;
      default:
        break;
    }
  }

  /* 
  ##############################
  チェックリストの順番変更処理
  ############################## 
  */
  const onDragEnd = (result) => {

    // ドロップ先がない場合、そのまま処理を抜ける
    if (!result.destination) return;

    // 配列の順番を入れ替える
    let movedCheckItem = props.reorder(
      props.checkList,          // 順番を入れ替えたい配列
      result.source.index,      // 元の配列での位置
      result.destination.index  // 移動先の配列での位置
    );

    props.setCheckList(movedCheckItem);
  };

  /* 
  ##############################
  チェックリスト追加処理
  ############################## 
  */
  const addCheckList = () => {

    // inputが空白ならそのまま何の処理も行わない
    if (inputValue === "") return;

    // 既存の配列に新たにチェックリストを加える
    // チェックリスト内要素の識別に使用されるidはstring(文字列)型でないと警告文が発生してしまう
    props.setCheckList([...props.checkList, ...[{id: `item-${itemCount}`, checkItem: inputValue}]]);
    
    // チェックリストを加えたのでカウントアップ
    setItemCount(itemCount + 1);

    // チェックリストに追加した後、入力内容をクリアする
    setInputValue("");
  }

  /* 
  ##############################
  チェックリスト内容変更処理
  ############################## 
  */
  const updateCheckList = (index, e) => {

    // slice()メソッドを使用してチェックリストのコピーを作成する
    const copyCheckList = props.checkList.slice();

    // index を使用して対象のチェックリストの内容を書き換える
    copyCheckList[index].checkItem = e.target.value;
    props.setCheckList(copyCheckList);
  }

  /* 
  ##############################
  チェックリスト削除処理
  ############################## 
  */
  const deleteCheckList = (index) => {
      
    // Array.from()メソッドは、反復可能オブジェクトや配列風オブジェクトから
    // シャローコピーされた、新しいArrayインスタンスを生成する
    const result = Array.from(props.checkList);

    // Array.splice()メソッドは、配列を操作するメソッド
    // 第2引数はオプション、第1引数に3、第2引数に2を指定した場合、3、4番目の要素を配列から取り出す
    result.splice(index, 1);
  
    props.setCheckList(result);
  }

  return (
    // onDragEnd={onDragEnd}→ドラッグ後のイベント処理、タスクの状態や順番を変更する
    <DragDropContext onDragEnd={onDragEnd}>
      <Droppable droppableId="droppable">
        {/* Droppableタグでsnapshotは以下のプロパティを持っている */}
        {/* snapshot.isDraggingOver:リスト上でアイテムがドラッグ中かどうか */}
        {(provided, snapshot) => (
          <div
            {...provided.droppableProps}
            ref={provided.innerRef}
            style={getListStyle(snapshot.isDraggingOver)}
          >
            {props.checkList.map((checkItem, index) => (
              <Draggable key={checkItem.id} draggableId={checkItem.id} index={index}>
                {/* Draggaleタグでsnapshotは以下のプロパティを持っている */}
                {/* snapshot.isDragging:アイテムがドラッグ中かどうか */}
                {(provided) => (
                  <div
                    ref={provided.innerRef}
                    {...provided.draggableProps}
                    {...provided.dragHandleProps}
                    style={getItemStyle(provided.draggableProps.style)}
                  >
                    <TodoCheckItem 
                      index={index}
                      checkItem={checkItem}
                      updateCheckList={updateCheckList}
                      deleteCheckList={deleteCheckList}
                    />
                  </div>
                )}
              </Draggable>
            ))}
            {/* ここにドラッグ可能なアイテムを配置 */}
            {provided.placeholder} 
            // 新しいチェックリストを追加するボタン/入力フォーム
            <button onClick={() => addCheckList()}>追加</button> 
            <input
              type="text" 
              value={inputValue}
              placeholder="新しいチェックリストを追加"
              onChange={(e) => setInputValue(e.target.value)}
              onKeyDown={(e) => onKeyDown(e, e.key, 0)}
              onCompositionStart={props.startComposition}
              onCompositionEnd={props.endComposition}
            >
            </input>
          </div>
        )}
      </Droppable>
    </DragDropContext>
  );
}

export default TodoCheckList;

ドラッグ&ドロップの基本設定

react-beautiful-dndを用いたドラッグ&ドロップを実装するための構文は以下になります。
詳細な処理は私自身把握しきれていないため、大まかな概要になります。

    <DragDropContext onDragEnd={onDragEnd}>
      <Droppable droppableId="droppable">
        {/* Droppableタグでsnapshotは以下のプロパティを持っている */}
        {/* snapshot.isDraggingOver:リスト上でアイテムがドラッグ中かどうか */}
        {(provided, snapshot) => (
          <div
            {...provided.droppableProps}
            ref={provided.innerRef}
            style={getListStyle(snapshot.isDraggingOver)}
          >
            {Arrays.map((Array, index) => (
              <Draggable key={Array.id} draggableId={Array.id} index={index}>
                {/* Draggaleタグでsnapshotは以下のプロパティを持っている */}
                {/* snapshot.isDragging:アイテムがドラッグ中かどうか */}
                {(provided) => (
                  <div
                    ref={provided.innerRef}
                    {...provided.draggableProps}
                    {...provided.dragHandleProps}
                    style={getItemStyle(provided.draggableProps.style)}
                  >
                    {/* 配列から1つひとつ取り出したドラッグ&ドロップする要素を設定する */}
                  </div>
                )}
              </Draggable>
            ))}
            {/* ここにドラッグ可能なアイテムを配置 */}
            {provided.placeholder} 
          </div>
        )}
      </Droppable>
    </DragDropContext>

TodoCheckItem

TodoCheckItem
const TodoCheckItem = (props) => {

  return (
    // チェックリストの各要素
    <>
      {/* 現在時点でチェックボックスは機能していない */}
      <input type="checkbox"></input>
      <input
        type="text"
        value={props.checkItem.checkItem}
        onChange={e => props.updateCheckList(props.index, e)}
      />
      <button onClick={() => props.deleteCheckList(props.index)}>削除</button>
    </>
  );
}

export default TodoCheckItem;

TodoList.js

TodoList.js
// TodoItemコンポーネントをインポートする
import TodoItem from "./TodoItem";

// ドラッグ&ドロップのライブラリreact-beautiful-dndをインポートする
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';

const TodoList = (props) => {

    /* 
    ##############################
    TODOリストの順番変更処理
    ############################## 
    */
    const onDragEnd = (result) => {

        // ドロップ先がない場合、そのまま処理を抜ける
        if (!result.destination) return;

        // 配列の順番を入れ替える
        let movedCheckItem = props.reorder(
        props.todoList,          // 順番を入れ替えたい配列
        result.source.index,      // 元の配列での位置
        result.destination.index  // 移動先の配列での位置
        );

        props.setTodoList(movedCheckItem);
    };

    /* 
    ##############################
    TODOリストのCSS
    ############################## 
    */
    const getListStyle = (isDraggingOver) => ({
        background: 'white',
        /* isDraggingOverの型は真偽値、true=ドラッグ中、false=ドラッグ中ではない  */
        /* border: isDraggingOver ? 'solid 5px lightgray' : 'solid 5px white', */
        textAlign: 'left',
    });
    
    /* 
    ##############################
    TODOアイテムのCSS
    ############################## 
    */
    const getItemStyle = (draggableStyle) => ({
        marginBottom: '0.5rem',

        ...draggableStyle
    });

    return (
        <DragDropContext onDragEnd={onDragEnd}>
            <Droppable droppableId="droppable">
                {/* Droppableタグでsnapshotは以下のプロパティを持っている */}
                {/* snapshot.isDraggingOver:リスト上でアイテムがドラッグ中かどうか */}
                {(provided, snapshot) => (
                    <div
                        {...provided.droppableProps}
                        ref={provided.innerRef}
                        style={getListStyle(snapshot.isDraggingOver)}
                    >
                        {/* ドラッグできる要素 */}
                        {props.todoList.map((todoItem, index) => (
                            <Draggable key={todoItem.id} draggableId={todoItem.id} index={index}>
                                {/* Draggaleタグでsnapshotは以下のプロパティを持っている */}
                                {/* snapshot.isDragging:アイテムがドラッグ中かどうか */}
                                {(provided) => (
                                    <div
                                        ref={provided.innerRef}
                                        {...provided.draggableProps}
                                        {...provided.dragHandleProps}
                                        style={getItemStyle(provided.draggableProps.style)}
                                    >
                                        <TodoItem
                                            todoItem={todoItem}
                                            toggleTodoItemStatus={props.toggleTodoItemStatus}
                                            changeTodoItem={props.changeTodoItem}
                                            deleteTodoItem={props.deleteTodoItem}                    
                                            getSingleTodosData={props.getSingleTodosData}
                                            setInputId={props.setInputId}    
                                            setInputTitle={props.setInputTitle}
                                            setInputMemo={props.setInputMemo}
                                            setCheckList={props.setCheckList}
                                            setPriority={props.setPriority}
                                            setDifficulty={props.setDifficulty}
                                            setInputDeadLine={props.setInputDeadLine}
                                            isShowModal={props.isShowModal} 
                                            setIsShowModal={props.setIsShowModal}
                                            setChangeFlg={props.setChangeFlg}
                                        />
                                    </div>
                                )}
                            </Draggable>
                        ))}
                        {/* ここにドラッグ可能なアイテムを配置 */}
                        {provided.placeholder} 
                    </div>
                )}
            </Droppable>
        </DragDropContext>
    )
}

export default TodoList;

TodoItem.js

TodoItem.js
import { useState, useEffect } from "react";

import TodoAdd from "./TodoAdd";

const TodoItem = (props) => {

    // 期限までの時間を設定する変数deadLine
    // deadLineを更新する関数setDeadLineを定義する
    const [deadLine, setDeadLine] = useState("");

    /* 
    ##############################
    完了/未完了変更処理
    ############################## 
    */
    const handleToggleTodoItemStatus = () => {
        props.toggleTodoItemStatus(props.todoItem.id, props.todoItem.done);
    }

    /* 
    ##############################
    TODOリスト削除処理
    ############################## 
    */
    const handleDeleteTodoItem = () => {
        props.deleteTodoItem(props.todoItem.id);
    }

    /* 
    ##############################
    TODO追加モーダル表示処理
    ############################## 
    */
    const handleChangeTodo = () => {
        
        props.setIsShowModal(true);   // TODO追加モーダルを表示する
        props.setChangeFlg(true);     // 編集/作成を切り替える
        props.setInputId(props.todoItem.id);
        props.setInputTitle(props.todoItem.title);
        props.setInputMemo(props.todoItem.memo);
        props.setCheckList(props.todoItem.checkList);
        props.setPriority(props.todoItem.priority);
        props.setDifficulty(props.todoItem.difficulty);
        props.setInputDeadLine(new Date(props.todoItem.deadLine)); // DatePickerに日付をDate型にしてから渡す
    }

    /* 
    ##############################
    期限までの残り時間を表示する
    ############################## 
    */
    const handleCountDown = () => {

        const nowDate = new Date();                                     // 本日の日時を取得
        const deadLineDate = new Date(props.todoItem.deadLine);         // 期限の日時を取得
        const diffDate = deadLineDate.getTime() - nowDate.getTime();    // 期限までの残り時間を取得

        // 期限が過ぎていない場合
        if (diffDate > 0) {
            setDeadLine(`${Math.floor(diffDate / 1000 / 60 / 60 / 24)}日後`);
        // 期限が過ぎている場合
        } else {
            setDeadLine("期限切れ")
        }
    } 

    // useEffectを使用してコンポーネントのマウント後に関数handleCountDownを実行する
    // useEffectの第2引数を空の依存配列[]にすることで初回の画面レンダリング時に関数handleCountDownを実行する
    useEffect(handleCountDown, []);

    /* 
    ##############################
    チェックボックス更新処理
    ############################## 
    */
    // 現在時点で関数handleChangeCheckは機能していない
    const handleChangeCheck = () => {

    };

    return (
        <div>
            <h3>{props.todoItem.title}</h3>
            <p>{props.todoItem.memo}</p>
            {/* map()を利用してcheckListの要素を1つひとつ取り出す */}
            {props.todoItem.checkList.map((checkItem) => (
                <label key={checkItem.id} style={{display: "block"}}>
                    <input type="checkbox" value={checkItem.chckItem} onChange={handleChangeCheck}/>
                    {checkItem.checkItem}
                </label>
            ))}
            <p>期限: 
                {/* 最初にタスクの完了/未完了を判定する、その後期限が過ぎていないか判定する */}
                {/* 上記の条件をクリアした場合、期限までの時間を表示する */}
                {props.todoItem.done ? "完了済み" : deadLine}
            </p>

            {/* ボタンをクリックすることで関数handleToggleTodoItemStatusを実行する */}
            {/* ボタンをクリックすることでTODOの状態(完了/未完了)を反転させる */}
            <button onClick={handleToggleTodoItemStatus}>
                {props.todoItem.done ? "未完了リストへ" : "完了リストへ"}
            </button>

            {/* ボタンをクリックすることで関数handleDeleteTodoItemを実行する */}
            {/* ボタンをクリックすることでTODOを削除する */}
            <button onClick={handleDeleteTodoItem}>削除</button>

            {/* ボタンをクリックすることで関数handleChangeTodoItemを実行する */}
            {/* 関数handleChangeTodoItemが実行されるとモーダル画面が表示される */}
            <button onClick={handleChangeTodo}>編集</button>
            {props.isShowModal && <TodoAdd />}
        </div>
    );
}

export default TodoItem;

編集する際の値の渡し方

TODOの内容を編集する際に以下のようにpropsで値を渡すことも考えましたが上手く値を渡すことができませんでした。そのため今回はuseStateを使用して値を設定しています。

            {/* 省略 */}
            <button onClick={handleChangeTodo}>編集</button>
            {props.isShowModal && 
                <TodoAdd
                    title={props.todoItem.title}
                    memo={props.todoItem.memo}
                    {/* 以下省略 */}
            />}

Title.js

Title.js
// TodoTitleコンポーネントを作成する
// 親コンポーネント(App)から受け取ったprops(as)の値により使用するタグを変更
const TodoTitle = (props) => {

    // asがh1ならば、タイトルはh1タグを使用
    if (props.as === "h1") {
      return <h1>{props.title}</h1>
  
    // asがh2ならば、タイトルはh2タグを使用
    } else if (props.as === "h2") {
      return <h2>{props.title}</h2>
  
    // それ以外ならば、タイトルはpタグを使用
    } else {
      return <p>{props.title}</p>
    }
  }

export default TodoTitle;

最後に

以上で各コードの解説を終了します。
React初心者ということもあり、あまり綺麗なコードとは言えませんが最後まで見ていただきありがとうございました。
次回以降はJavaScriptからTypeScriptに変換していきます。

4
3
1

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
4
3