iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

React でToDoListを作ってみた!

はじめに

 こんにちは、iimon新卒エンジニアの「ばんり」です。

本記事はアドベントカレンダー11日目の記事になります!

最近業務でReactを使うことが多くなり、Reactをお勉強中です!今回は私がReactを勉強する際、最初に作ったToDoリストについて紹介します。 今回はReact 16.8 で追加された機能Hooksを使っています!

完成イメージ:

コンポーネントの構成について

 まず、最初にコンポーネントの構成について考えてみましょ。 コンポーネントの分け方は人それぞれですが、今回私は下記のイメージのようにコンポーネントを分けて実装しました! コンポーネントの処理:

  • Header:入力ボックスの表示、内容の入力、リストの追加に使用。
  • List:リストの表示に使用。
  • Item:ToDoリストの表示、チェック機能、削除機能の実装に使用。
  • Footer:チェックボックス、チェック済みデータ、削除ボタンを表示するために使用されます。

App.jsx

import "./App.css";
import Footer from "./components/Footer";
import Header from "./components/Header";
import List from "./components/List";

function App() {
  const todo = [
    {
      id: "1",
      name: "勉強",
      done: true,
    },
    {
      id: "2",
      name: "寝る",
      done: false,
    },
    {
      id: "3",
      name: "買い物",
      done: true,
    },
  ];

  return (
    <div className="todo-co">
      <div className="todo-wa">
        <Header />
        <List />
        <Footer />
      </div>
    </div>
  );
}

export default App;

Header.jsx

const Header = () => {
  return (
    <div className="header">
      <input type="text" placeholder="予定を入力してください" />
      <button>追加</button>
    </div>
  );
};

export default Header;

List.jsx

import Item from "./Item";

const List = () => {
  return (
    <ul>
      <Item />
    </ul>
  );
};
export default List;

Item.jsx

const Item = () => {
  return (
    <li>
      <label>
        <input type="checkbox" />
        <span></span>
      </label>
      <div className="btn-wa">
        <button>削除</button>
      </div>
    </li>
  );
};

export default Item;

Footer.jsx

const Footer = () => {
  return (
    <div className="todo-footer">
      <label>
        <input type="checkbox" />
      </label>
      <span>
        <span>完成済み</span> / 全部
      </span>
      <div className="btn-wa2">
        <button>完成済みを空にする</button>
      </div>
    </div>
  );
};

export default Footer;

以上のようにコンポーネントの準備ができたら実装始めていきましょ。

準備

App.js

const App = () => {
  const todo = [
    {
      id: "1",
      name: "勉強",
      done: true,
    },
    {
      id: "2",
      name: "寝る",
      done: false,
    },
    {
      id: "3",
      name: "買い物",
      done: true,
    },
  ];
  const [todos, setTodo] = useState(todo);

  return (
    <div className="todo-co">
      <div className="todo-wa">
        <Header />
        <List todos={todos} />
        <Footer />
      </div>
    </div>
  );
}

export default App;

初期のToDoリスト項目を定義し、useState を使用して、todos 状態を管理します。 Itemコンポーネントでそれぞれの項目を表示させているので、App→List→Itemの順番で値を渡していきます。

List.jsx

import Item from "../item";

const List = ({todos}) => {
  return (
    <ul>
      {todos.map((todo) => {
        return <Item key={todo.id}{...todo} />;
      })} 
    </ul>
  );
};
export default List;

Listから受け取ったtodoのnameを画面に表示させます。

Item.jsx

const Item = ({name}) => {
  return (
    <li>
      <label>
        <input type="checkbox" />
        <span>{name}</span>
      </label>
      <div className="btn-wa">
        <button>删除</button>
      </div>
    </li>
  );
};

export default Item;

Listコンポーネントでtodosを受け取りMapメソッドを使ってそれぞれの値をItemコンポーネントで返します。

注意:今回の場合Itemのkeyはindexではなく、与えられたidを使うことをお勧めします。 その理由について以下の記事をご覧ください!

React Mapメソッド使う際に引っかかった話 - iimon TECH BLOG

イメージのようにできたら準備完了です!

Headerの実装

先ほど話したようにHeaderは入力ボックスの表示、内容の入力、リストの追加に使用されます。今回のロジックに当たる部分はリストの追加になります。まず、実装する前に整理しましょ。 一個前の内容ではデフォルトのリストはすでにItemコンポーネントで表示していました。そのリストは親コンポーネントAppで状態管理しているため、Headerの入力内容はAppに渡し、更新するたびにAppで保持している状態も更新します。 ここで疑問になるのはどうやってHerder(子コンポーネント)からApp(親コンポーネント)に値を渡すか? 今回は親にコールバック関数を作り、propsとして子コンポーネントに渡しました!具体的な実装方法をみていきましょう イメージで表すとこんな感じです

Header.jsx

import { useState } from "react";
import { nanoid } from "nanoid";

const Header = ({ addTodo }) => {
  const [inpText, setInpText] = useState("");

  // 入力されたテキストを保存する
  const saveInputText = (e) => {
    setInpText(e.target.value);
  };

  // 追加ボタンがクリックされた時の処理
  const handleClick = () => {
    const todoObj = { id: nanoid(), name: inpText, done: false };
    // 親コンポーネントのaddTodo関数にtodoObjを渡す
    addTodo(todoObj);
    // 入力欄を空にする
    setInpText("");
  };

  return (
    <div className="header">
      <input
        type="text"
        placeholder="予定を入力してください"
        onChange={saveInputText}
        value={inpText}
      />
      <button onClick={handleClick}>追加</button>
    </div>
  );
};

export default Header;

まず、inputに入力した値をuseState("")を使って保持する関数saveInputTextを作りました、次にボタンが押された時に処理する関数handleClickを作りました。 handleClick関数はinputに入力した値をpropsで受け取った親コンポーネントのaddToDoに引数として渡します。

一個前の内容ではidをkeyにする話は覚えてますでしょうか、当然今回も親に渡すデータはただのinputの値だけではありません。デフォルトのリストと同じようにそれぞれのリストを識別するためのidとリストが完成したかを判断するdoneを渡す必要があります。

今回は「Nano ID」という任意の重複しない文字列を生成するライブラリーを使ってidに当てました、また新しいリストはまだ完成していないのでdoneをfalseにします。

nanoidインストール方法:

https://www.npmjs.com/package/nanoid

npm i nanoid

をターミナルに貼れば簡単にインストールすることができます!

App.isx

const [todos, setTodo] = useState(todo);

  //todoObjを受け取り、todosに追加
  const addTodo = (todoObj) => {
    const newTodo = [todoObj, ...todos];
    setTodo(newTodo);
  };

  return (
    <div className="todo-co">
      <div className="todo-wa">
        <Header addTodo={addTodo} />
        <List todos={todos} />
        <Footer />
      </div>
    </div>
  );
}

addTodo関数を作り、Headerから受け取った値を既存のリストの前に追加してセットします。 これでHeaderの部分は完成です✨

Itemのスタイル実装

Itemにマウスを当てた時背景色が変わるのとボタンが表示されることがわかります。CSSのhoverを使ったら簡単に実装できますが、せっかくなので今回はreactでやってみました

Item.jsx

import { useState } from "react";

const Item = ({ name }) => {
  const [flag, setFlag] = useState(false);

  const handleChangeStyle = (flag) => {
    return () => {
      setFlag(flag);
    };
  };
  return (
    <li
      style={{ backgroundColor: flag ? "#ddd" : "white" }}
      onMouseEnter={handleChangeStyle(true)}
      onMouseLeave={handleChangeStyle(false)}
    >
      <label>
        <input type="checkbox" />
        <span>{name}</span>
      </label>
      <div className="btn-wa">
        <button
          style={{ opacity: flag ? "1" : "0" }}
          onMouseEnter={handleChangeStyle(true)}
          onMouseLeave={handleChangeStyle(false)}
        >
          削除
        </button>
      </div>
    </li>
  );
};

export default Item;

useStateを使ってflagの値を保持します。 onMouseEnterとonMouseLeaveのイベントでそれぞれ"true"と"false"をhandleChangeStyle に渡しその値をセットします。 これで完成です✨

Itemの実装

ItemはToDoリストの表示、チェック機能、削除機能の実装に使用されます、今回の処理にあたる削除機能を実装していきます。

Item.jsx

import { useState } from "react";

const Item = ({id,name,deleteTodo}) => {
  const [flag, setFlag] = useState(false);

  const handleChangeStyle = (flag) => {
    return () => {
      setFlag(flag);
    };
  };

  const handelDelete = (id) => {
    return () => {
      if(window.confirm('削除してもよろしいでしよか?')){
        deleteTodo(id);
      }
    };
  };

  return (
    <li
      style={{ backgroundColor: flag ? "#ddd" : "white" }}
      onMouseEnter={handleChangeStyle(true)}
      onMouseLeave={handleChangeStyle(false)}
    >
      <label>
        <input type="checkbox" />
        <span>{name}</span>
      </label>
      <div className="btn-wa">
        <button
          style={{ opacity: flag ? "1" : "0" }}
          onMouseEnter={handleChangeStyle(true)}
          onMouseLeave={handleChangeStyle(false)}
          onClick={handelDelete(id)}
        >
          削除
        </button>
      </div>
    </li>
  );
};

export default Item;

まず最初にアイテムのIDと状態を取得し、指定されたTodoを更新するようにAppコンポーネントに通知します。Headerと同じ状況なのでAppにコールバック関数を作ります。一番最初にListからpropsでもらったtodoのnameに加えtodoのidも受け取ります。

App.jsx

  const [todos, setTodo] = useState(todo);

  //todoObjを受け取り、todosに追加
  const addTodo = (todoObj) => {
    const newTodo = [todoObj, ...todos];
    setTodo(newTodo);
  };

  //idを受け取り、該当するidのtodoを削除
   const deleteTodo = (id) => {
    const newTodo = todos.filter((todoObj) => {
      return todoObj.id !== id;
    });
    setTodo(newTodo);
  };

  return (
    <div className="todo-co">
      <div className="todo-wa">
        <Header addTodo={addTodo} />
        <List todos={todos} deleteTodo={deleteTodo}/>
        <Footer />
      </div>
    </div>
  );
}

deleteTodo関数を作り、 該当するidのtodoをフィルターして、それ以外をセットします。

List.jsx

import Item from "./Item";

const List = ({ todos, deleteTodo, setDone }) => {
  return (
    <ul>
      {todos.map((todo) => {
        return (
          <Item
            key={todo.id}
            {...todo}
            deleteTodo={deleteTodo}
          />
        );
      })}
    </ul>
  );
};
export default List;

*ListコンポーネントでdeleteTodoを受け取ってからItemに渡すの忘れずに

これでItemの削除機能の部分は完成です✨

Footerの実装(チェックボックスに状態を持たせる)

 Footerの処理に該当する部分に、チェック済み、全部の数のデータ表示と、完成したtodoリストを空にするのがあります。まず、チェック済み、完成済みのデータ表示をやっていきます。ここで一旦整理します。チェック済みはチェックボックスにチェック入れたものを取得します。しかし画面上でチェック入れればいいと思いますが、それだとうまくいきません。それはただ単に画面上でチェック入れただけで、状態持っていないので、何も変わりません。

画面上:

実際の状態:

上の画像でわかるように、全部にチェック入れても実際trueになっているのは最初にdoneをtrueに設定した勉強と買い物のリストだけです。特定のアイテムがチェックしたか判断するためまずそれぞれのidを取得し、親コンポーネントAppにidが〇〇のアイテムがチェックされたと通知する必要があります。

Item.jxs

import { useState } from "react";

const Item = ({id,name,deleteTodo,setDone}) => {
  const [flag, setFlag] = useState(false);

  const handleChangeStyle = (flag) => {
    return () => {
      setFlag(flag);
    };
  };

  const handelDelete = (id) => {
    return () => {
      if(window.confirm('削除してもよろしいでしよか?')){
        deleteTodo(id);
      }
    };
  };

  
  const handelCheck = (id) => {
    return (event) => {
      setDone(id, event.target.checked);
    };
  };

  return (
    <li
      style={{ backgroundColor: flag ? "#ddd" : "white" }}
      onMouseEnter={handleChangeStyle(true)}
      onMouseLeave={handleChangeStyle(false)}
    >
      <label>
        <input type="checkbox"  onChange={handelCheck(id)} checked={done} />
        <span>{name}</span>
      </label>
      <div className="btn-wa">
        <button
          style={{ opacity: flag ? "1" : "0" }}
          onMouseEnter={handleChangeStyle(true)}
          onMouseLeave={handleChangeStyle(false)}
          onClick={handelDelete(id)}
        >
          削除
        </button>
      </div>
    </li>
  );
};

export default Item;

チェックボックスの状態が変更されたときに呼び出される関数 handelCheckを返します。 返された関数は、event オブジェクトを受け取り、event.target.checked を使ってチェックボックスの新しい状態を取得します。 setDone 関数を呼び出し、id と新しい done 状態を渡します。

App.jsx

 const [todos, setTodo] = useState(todo);

  //todoObjを受け取り、todosに追加する
  const addTodo = (todoObj) => {
    const newTodo = [todoObj, ...todos];
    setTodo(newTodo);
  };

  //idを受け取り、該当するidのtodoを削除する
   const deleteTodo = (id) => {
    const newTodo = todos.filter((todoObj) => {
      return todoObj.id !== id;
    });
    setTodo(newTodo);
  };

  //idとdoneを受け取り、該当するidのdoneを更新する
  const setDone = (id, done) => {
    const newTodo = todos.map((todoObj) => {
      if (todoObj.id === id) {
        return { ...todoObj, done: done };
      } else {
        return todoObj;
      }
    });
    setTodo(newTodo);
  };

  return (
    <div className="todo-co">
      <div className="todo-wa">
        <Header addTodo={addTodo} />
        <List todos={todos} deleteTodo={deleteTodo} setDone={setDone}/>
        <Footer />
      </div>
    </div>
  );
}

setDone関数を作り、todoObj.id が指定された id と一致する場合、その todoObj の done プロパティを更新します。 一致しない場合は、元の todoObj をそのまま返します。これにより、チェックボックスの状態が変更されたときに、 対応する todo の done 状態が更新されます。 これでチェックしたアイテムは状態も一緒に更新されます✨

いざFooterのチェック済み、完成済みの機能を実装していきます! Footer.jsx

const Footer = ({ todos }) => {

  const total = todos.length;

    const checkAllTodo = () => {
      let count = 0;
      todos.forEach((todoObj) => {
        if (todoObj.done === true) {
          count += 1;
        }
      });
      return count;
    };

  return (
    <div className="todo-footer">
      <label>
        <input
          type="checkbox"
        />
      </label>
      <span>
        <span>完成済み{checkAllTodo()}</span> / 全部{total}
      </span>
      <div className="btn-wa2">
        <button>完成済みを空にする</button>
      </div>
    </div>
  );
};

export default Footer;

全体のtodoはlengthをとって完成です。次にcheckAllTodoの関数を作り、 配列内の完了した todo 項目の数をカウントします。 これにより、完了した todo 項目の数と全体の todo 項目の数が表示されます。✨

Footerのチェック機能

上の画像のようにアイテムが全部チェック済みの時、Footerのチェックも入る、またチェック外すと、Footerのチェックも取れる、連動するようにしていきます。

Footer.jsx

const Footer = ({ todos,allChecked }) => {

  const total = todos.length;

    const checkAllTodo = () => {
      let count = 0;
      todos.forEach((todoObj) => {
        if (todoObj.done === true) {
          count += 1;
        }
      });
      return count;
    };

    const allCheck = (event) => {
      allChecked(event.target.checked);
    };

  return (
    <div className="todo-footer">
      <label>
        <input
          type="checkbox"
          onChange={allCheck}
          checked={total === checkAllTodo() && total !== 0 ? true : false}
        />
      </label>
      <span>
        <span>完成済み{checkAllTodo()}</span> / 全部{total}
      </span>
      <div className="btn-wa2">
        <button>完成済みを空にする</button>
      </div>
    </div>
  );
};

export default Footer;

allCheck関数を作り、チェックボックスの状態が変更されたときに呼び出されます。 event オブジェクトから target.checked プロパティを取得し、その値を allChecked 関数に渡します。 total と checkAllTodo() の結果に基づいて、チェックボックスがチェックされているかどうかを決定するため 以下のコードを追加します。

checked={total === checkAllTodo() && total !== 0 ? true : false}

App.jsx

 const [todos, setTodo] = useState(todo);

  //todoObjを受け取り、todosに追加
  const addTodo = (todoObj) => {
    const newTodo = [todoObj, ...todos];
    setTodo(newTodo);
  };

  //idを受け取り、該当するidのtodoを削除
   const deleteTodo = (id) => {
    const newTodo = todos.filter((todoObj) => {
      return todoObj.id !== id;
    });
    setTodo(newTodo);
  };

  //idとdoneを受け取り、該当するidのdoneを更新
  const setDone = (id, done) => {
    const newTodo = todos.map((todoObj) => {
      if (todoObj.id === id) {
        return { ...todoObj, done: done };
      } else {
        return todoObj;
      }
    });
    setTodo(newTodo);
  };

  //doneを受け取り、全てのtodoのdoneを更新
  const allChecked = (done) => {
    const newTodo = todos.map((todoObj) => {
      return { ...todoObj, done: done };
    });
    setTodo(newTodo);
  };

  return (
    <div className="todo-co">
      <div className="todo-wa">
        <Header addTodo={addTodo} />
        <List todos={todos} deleteTodo={deleteTodo} setDone={setDone}/>
        <Footer todos={todos} allChecked={allChecked}/>
      </div>
    </div>
  );
}

allChecked関数を作り引数として受け取った done の値に基づいて、todos 配列内のすべてのタスクの done を更新します。 以上でFooterとItem同士のチェックの連動は完成です。✨

Footerの削除機能

いよいよ最後の機能になります。Footerの削除機能作っていきましよーー

Footer.jsx

const Footer = ({ todos,allChecked,clearAllDone}) => {

  const total = todos.length;

    const checkAllTodo = () => {
      let count = 0;
      todos.forEach((todoObj) => {
        if (todoObj.done === true) {
          count += 1;
        }
      });
      return count;
    };

    const allCheck = (event) => {
      allChecked(event.target.checked);
    };

    const handelClearAllDone = () => {
      clearAllDone();
    };


  return (
    <div className="todo-footer">
      <label>
        <input
          type="checkbox"
          onChange={allCheck}
          checked={total === checkAllTodo() && total !== 0 ? true : false}
        />
      </label>
      <span>
        <span>完成済み{checkAllTodo()}</span> / 全部{total}
      </span>
      <div className="btn-wa2">
        <button onClick={handelClearAllDone}>完成済みを空にする</button>
      </div>
    </div>
  );
};

export default Footer;

App.jsx

 const [todos, setTodo] = useState(todo);

  //todoObjを受け取り、todosに追加
  const addTodo = (todoObj) => {
    const newTodo = [todoObj, ...todos];
    setTodo(newTodo);
  };

  //idを受け取り、該当するidのtodoを削除
  const deleteTodo = (id) => {
    const newTodo = todos.filter((todoObj) => {
      return todoObj.id !== id;
    });
    setTodo(newTodo);
  };

  //idとdoneを受け取り、該当するidのdoneを更新
  const setDone = (id, done) => {
    const newTodo = todos.map((todoObj) => {
      if (todoObj.id === id) {
        return { ...todoObj, done: done };
      } else {
        return todoObj;
      }
    });
    setTodo(newTodo);
  };

  //doneを受け取り、全てのtodoのdoneを更新
  const allChecked = (done) => {
    const newTodo = todos.map((todoObj) => {
      return { ...todoObj, done: done };
    });
    setTodo(newTodo);
  };

  //全てのdoneがtrueのtodoを削除
  const clearAllDone = () => {
    const newTodo = todos.filter((todoObj) => {
      return todoObj.done === false;
    });
    setTodo(newTodo);
  };

  return (
    <div className="todo-co">
      <div className="todo-wa">
        <Header addTodo={addTodo} />
        <List todos={todos} deleteTodo={deleteTodo} setDone={setDone} />
        <Footer
          todos={todos}
          allChecked={allChecked}
          clearAllDone={clearAllDone}
        />
      </div>
    </div>
  );
}

handelClearAllDone 関数が呼び出されたときに、clearAllDone 関数が実行され、doneがfalseになるものを残し、完了した todo 項目が一括で削除されます。 これで全部の機能が完成です👏

まとめ

 ここまで読んでくださりありがとうございます!今までなんとなくでpropsやuseStateを使っていて、すごく曖昧でした。TodoListをやることによって基礎が固められ、より理解が深めました。業務中で実際使う時もコンポーネントの設計、propsやuseStateを即戦できるようになりました。皆さんもある程度のReactの基本を終えた際にぜひやってみてください。 この記事を読んで興味を持って下さった方がいらっしゃれば、カジュアルにお話させていただきたいです。是非ご応募をお願いいたします!

Wantedly / Green

次は仕事熱心な超真面目のやーさんです、どんな記事書くか楽しみにしています!!!

参考文献

www.npmjs.com

056_尚硅谷_react教程_TodoList案例_静态组件_哔哩哔哩_bilibili