🐣

今更ながら React で ToDo リスト作成してみる

2024/01/06に公開

はじめに

はじめまして、Zenn での初投稿になります。
今まで Reactでサイトやらアプリやら公私含めて色々作ってきましたが、新年ですし初心に戻って ToDo リストを作ってみたのです。
ところが、実装してみると一部詰まったので備忘録がてら書いていこうと思います。

  • 想定読者
    • 駆け出しエンジニアの方
    • ReactNextなどモダンなフロントエンドに関心のある Web デザイナー・コーダーの方々
    • リスキリングを考えている非エンジニアの方

ひとまず完成品

書いてみて

ToDo リストを作るのはReactの学び始め以来で、当時お世話になったのは下記書籍でした。
有名な書籍ですよね。

https://www.amazon.co.jp/モダンJavaScriptの基本から始める-React実践の教科書-(最新ReactHooks対応)-じゃけぇ(岡田-拓巳)-ebook/dp/B09BV2HGN3

色々な方がレビューしていると思いますが、筆者的にはバイブルと呼べるほど分かりやすくて楽しくReact入門できた良書でした。

書籍にて ToDo リストの制作があるのですが、筆者の手元にあるものではメモの「追加」と「削除」のみのシンプルなものでした。

今回、上記機能に「チェック」機能を追加しようとして詰まりました。

具体的には当初、チェックされた状態をuseStateで管理しようとしていました。
Reactにはstateという仕組みがあり、直訳通り「要素の状態」を指します。ポケモンでいう毒やマヒ、やけどといったイメージとほぼ同じです。

.
..
const [checked, setChecked] = useState<boolean>(false);
//...
return (
//...
<li key={i} style={checked ? checkedStyle : defaultStyle}>
<input type="checkbox" onChange={() => setChecked(!checked)} />
..
.

しかしこれだとチェックすると、リストの全項目がcheckedされて全リストにcheckedStyleのスタイルがあたってしまいました。

なるほど、ひとつのstateで一元管理のような状態になっているから、一つをチェックすると全てが更新(オンになる)されるのか。
では、クリックしたリストに対して特定のスタイルをあててやろう!

.
..
const toggleCheckedClass = (inputElm:HTMLInputElement) => {
  const parentLiElm:HTMLLiElement | null = inputElm.closest('li');
  parentLiElm?.classList.toggle('checked');
}
//...
<input type="checkbox" onChange={(inputElm:ChangeEvent<HTMLInputElement>) => toggleCheckedClass(inputElm.currentTarget)} />
..
.

よし!これで意図通り、クリックしたリストにのみスタイルがあたった。
……と思ったのですが、何と当該リストを削除するとその下のリストにcheckedが付いてしまいました。

  • 以下のような状況
    • 掃除 ← 掃除が済んだのでチェックして削除
    • 洗濯
    • 買い物
      ...
      ..
      .
    • 洗濯 ← 掃除リストは消えたものの下にあった洗濯リストにチェックが付いてしまう
    • 買い物

ここが詰まったところです。恐らく、リストは更新されるもののリアルDOMにスタイルを指定する形にしていたために起きたことだと思っています。

上記のトラブルを解消するために、筆者はオブジェクト(のstate)にして管理することにしました。

type todoListType = {
    item: string;
    checked: boolean;
}
..
.
/* リスト(オブジェクトのstate)*/
const [todoList, setTodoList] = useState<todoListType[]>([]);
.
..
const newAry: todoListType = {
  item: inputTxt, // inputTxt は 入力欄に入力した内容
  checked: false // 初期値 false
}
setTodoList((_prevTodoList) => [...todoList, newAry]); // リストを更新
..
.
const updateTodoList: (item: todoListType, index: number, bool: boolean) => void = (item: todoListType, index: number, bool: boolean) => {
  const newAry: todoListType = {
    item: item.item,
    checked: bool
  }
  const shallowCopy: todoListType[] = [...todoList];
  shallowCopy.splice(index, 1, newAry); // 更新したタスクを更新前の内容と差し替え
  setTodoList((_prevTodoList) => shallowCopy); // リストを更新
}

// item.checked の値に応じたリストチェックのオンオフ切替
const checkedSignal: (item: todoListType, index: number) => void = (item: todoListType, index: number) => {
  /* 既にチェックされていたらチェックを外す */
  if (item.checked === true) updateTodoList(item, index, false);
  /* まだチェックされていなければチェックする */
  else updateTodoList(item, index, true);
}
..
.
{
  todoList.map((item, i) => (
    <li key={i} style={item.checked ? checkedStyle : defaultStyle}>
      <label>
        <input
          type="checkbox"
          style={{ appearance: "none" }}
          onChange={() => checkedSignal(item, i)}
        />
        No.{i + 1}{item.item}
      </label>
      <button type="button" onClick={() => deleteItem(i)}>×</button>
    </li>
  ))
}

冗長なコードになっている気もしますが、これにて意図した挙動になりました!
冒頭の完成品を今一度こちらに置いておきます。

さいごに

ここまで読んでいただき、ありがとうございました。

色々な書籍や技術系記事といった各種情報には「インプット後のアウトプットが大切」という話が多いと思います。
ToDo リストはそういった場合に候補に挙がるものなので、この記事の内容がどなたかの参考になれば幸いです。

今後も定期的に記事を投稿していこうと思います。

Discussion