JavaScript | 1 日 120 分 × 7 日アプリ学習:ToDoアプリ(設計力強化編)

JavaScript
スポンサーリンク

この7日間のゴールと全体像

この 7 日間は「ToDoアプリ」を題材に、ただ動くものではなく「設計できる人」になることを狙います。
テーマは次の 3 つです。

  • 状態管理(state):アプリの「今の状態」をどう持つか
  • データ構造設計:タスクを「配列+オブジェクト」でどう表現するか
  • 再描画ロジック:状態が変わったときに画面をどう描き直すか

技術的には「ライブラリなしの素の JavaScript(バニラ JS)」で書きます。
ただし考え方は、そのまま React / Vue などのフレームワークに直結します。

毎日 120 分は、ざっくり「60 分で理解」「60 分で手を動かす」をイメージしてください。


1 日目:状態(state)とデータ構造の設計を固める

ToDoアプリの「状態」とは何か

まず、ToDo アプリが「今どうなっているか」を言葉にします。
この情報の集まりが state(状態)です。

状態として必要なものは最低でも次です。

  • 画面に存在するタスクの一覧
  • タスクごとの情報(id, title, completed)
  • 今のフィルタ状態(全件 / 完了 / 未完了)

ここで大事なのは、「DOM(HTML 要素)を状態として持たない」ことです。
状態は「純粋なデータ」として JavaScript の変数に持ち、画面は「状態の結果」として描画します。

タスクのデータ構造を決める(重要部分)

タスク 1 件をオブジェクトで表現します。

const task = {
  id: 1,
  title: "牛乳を買う",
  completed: false,
};
JavaScript

この 3 つにはそれぞれ理由があります。

  • id
    削除・完了切り替えで「どのタスクか」を識別するための一意な番号。
    配列のインデックスを直接使うと、削除時などにズレてややこしくなるので、id を持たせる方が安全です。
  • title
    タスクの内容。文字列で OK。
  • completed
    true なら完了、false なら未完了。
    「完了日」などを後から追加したい場合も、このフラグは土台として生き続けます。

複数のタスクは配列にします。

let tasks = [
  { id: 1, title: "牛乳を買う", completed: false },
  { id: 2, title: "メール返信", completed: true },
];
JavaScript

これで「配列+オブジェクト」の構造になります。

全体の state オブジェクトを定義する

状態をひとまとめにしたオブジェクトを用意します。

const state = {
  tasks: [],
  filter: "all", // "all" | "done" | "todo"
};
JavaScript

state の中に

  • tasks(全タスク)
  • filter(表示モード)

を入れることで、「アプリの状態は state を見ればわかる」形にします。

この「状態を 1 カ所に集める」という考え方が、設計力を一気に上げてくれます。


2 日目:HTML の骨組みと「状態から再描画する」関数の雛形

最小限の HTML を用意する

index.html を次のように用意します(CSS はかなり簡略化)。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <title>ToDo アプリ</title>
  <style>
    body { font-family: sans-serif; padding: 20px; }
    .todo-item { display: flex; align-items: center; margin-bottom: 4px; }
    .todo-title { flex-grow: 1; margin-left: 8px; }
    .todo-title.completed { text-decoration: line-through; color: #888; }
    button { margin-left: 4px; }
    .filters button.active { font-weight: bold; }
  </style>
</head>
<body>
  <h1>ToDo アプリ</h1>

  <div>
    <input id="todo-input" type="text" placeholder="タスクを入力..." />
    <button id="add-button">追加</button>
  </div>

  <div class="filters">
    <button data-filter="all" class="active">全件</button>
    <button data-filter="todo">未完了</button>
    <button data-filter="done">完了</button>
  </div>

  <div id="todo-list"></div>

  <script src="app.js"></script>
</body>
</html>

ポイントは、JavaScript から触る要素に id や data 属性を付けておくことです。

JavaScript 側の基本構造

app.js に次を書きます。

const state = {
  tasks: [],
  filter: "all",
};

const inputEl = document.getElementById("todo-input");
const addButtonEl = document.getElementById("add-button");
const listEl = document.getElementById("todo-list");
const filterButtons = document.querySelectorAll(".filters button");

let nextId = 1;
JavaScript

ここまでは「状態と DOM を掴む」ところまで。
次は「state から DOM を再描画する render 関数」を作ります。

render 関数の雛形(重要)

function render() {
  listEl.textContent = "";

  const filteredTasks = getFilteredTasks();

  filteredTasks.forEach((task) => {
    const itemEl = document.createElement("div");
    itemEl.className = "todo-item";

    const checkboxEl = document.createElement("input");
    checkboxEl.type = "checkbox";
    checkboxEl.checked = task.completed;

    const titleEl = document.createElement("div");
    titleEl.className = "todo-title";
    titleEl.textContent = task.title;
    if (task.completed) {
      titleEl.classList.add("completed");
    }

    const deleteButtonEl = document.createElement("button");
    deleteButtonEl.textContent = "削除";

    itemEl.appendChild(checkboxEl);
    itemEl.appendChild(titleEl);
    itemEl.appendChild(deleteButtonEl);

    listEl.appendChild(itemEl);
  });

  updateFilterButtons();
}
JavaScript

この render のポイントを深掘りします。

  • listEl.textContent = “”
    一度リストを空にしてから、state に基づいて全て描き直す。
    「部分更新」よりシンプルでバグりにくい設計です。
  • filteredTasks.forEach で、現在のフィルタに応じたタスクだけを描画。
    「表示対象を決めるロジック」と「DOM を作るロジック」を分けやすくなります。
  • DOM 生成は、あくまで state のコピー。
    DOM に書き換えた結果を「真実」とみなさず、state を真実とみなします。

この「状態から毎回全部描画」が、設計力強化の核になります。


3 日目:タスク追加ロジックと「状態を書き換えてから render」の流れ

タスクを追加する関数

タスク追加は、「state を更新する関数」に切り出します。

function addTask(title) {
  const trimmed = title.trim();
  if (!trimmed) {
    return;
  }

  const newTask = {
    id: nextId++,
    title: trimmed,
    completed: false,
  };

  state.tasks.push(newTask);
}
JavaScript

ここで意識してほしいのは、

  • DOM に直接新しい要素を追加しない
  • 必ず「state.tasks に push → render()」の順にする

という流れです。

イベントハンドラから addTask を呼び出す

addButtonEl.addEventListener("click", () => {
  addTask(inputEl.value);
  inputEl.value = "";
  render();
});

inputEl.addEventListener("keydown", (event) => {
  if (event.key === "Enter") {
    addTask(inputEl.value);
    inputEl.value = "";
    render();
  }
});
JavaScript

ここでも「状態変更 → 再描画」です。

  • addTask で state を変える
  • render で画面を状態に合わせる

イベントハンドラの中に DOM 操作を直接書かず、「状態変化の窓口」だけ通るようにする。
これが中級に上がるための感覚です。

getFilteredTasks の実装

render 内で使っていた getFilteredTasks を実装します。

function getFilteredTasks() {
  if (state.filter === "all") {
    return state.tasks;
  } else if (state.filter === "done") {
    return state.tasks.filter((task) => task.completed);
  } else if (state.filter === "todo") {
    return state.tasks.filter((task) => !task.completed);
  }
}
JavaScript

ここでは

  • データそのもの(state.tasks)は変えない
  • 表示用に「フィルタ済みの配列」を返す

という設計にしています。
フィルタは「ビューの問題」であり、「データの削除」ではないことを意識してください。


4 日目:完了切り替え・削除ロジックとイベントの紐付け

完了状態の切り替えロジック(重要)

「チェックボックスをクリックしたら、対応するタスクの completed を反転」
という動きを作ります。

ロジック自体はシンプルです。

function toggleTaskCompleted(id) {
  const task = state.tasks.find((t) => t.id === id);
  if (!task) return;
  task.completed = !task.completed;
}
JavaScript

ここでのポイントは

  • 「どのタスクか」は id で特定する
  • 配列の index に依存しない

ことです。
index ベースで操作すると、削除・並び替えのたびに壊れやすくなります。

削除ロジック

削除は「指定 id のタスクを除いた新しい配列を作る」書き方にします。

function deleteTask(id) {
  state.tasks = state.tasks.filter((t) => t.id !== id);
}
JavaScript

filter を使って、条件に合うものだけ残すパターンです。
この書き方は「元の配列を直接いじる」のではなく、「新しい配列を作って置き換える」ので、
状態の変化が読みやすくなります。

DOM イベントとタスク ID の紐付け

問題は、「クリックされた DOM と、タスクの id をどう紐付けるか」です。
中級としては「data 属性を使う」のがきれいです。

render を少し修正します。

filteredTasks.forEach((task) => {
  const itemEl = document.createElement("div");
  itemEl.className = "todo-item";
  itemEl.dataset.id = String(task.id); // ここで紐付け

  const checkboxEl = document.createElement("input");
  checkboxEl.type = "checkbox";
  checkboxEl.checked = task.completed;

  const titleEl = document.createElement("div");
  titleEl.className = "todo-title";
  titleEl.textContent = task.title;
  if (task.completed) {
    titleEl.classList.add("completed");
  }

  const deleteButtonEl = document.createElement("button");
  deleteButtonEl.textContent = "削除";

  itemEl.appendChild(checkboxEl);
  itemEl.appendChild(titleEl);
  itemEl.appendChild(deleteButtonEl);

  listEl.appendChild(itemEl);
});
JavaScript

イベントハンドラは「イベント委譲」を使うときれいです。
親要素に 1 個だけ付けて、子要素で何が起こったかを見るパターンです。

listEl.addEventListener("click", (event) => {
  const target = event.target;

  if (target.tagName === "INPUT" && target.type === "checkbox") {
    const itemEl = target.closest(".todo-item");
    const id = Number(itemEl.dataset.id);
    toggleTaskCompleted(id);
    render();
  }

  if (target.tagName === "BUTTON") {
    const itemEl = target.closest(".todo-item");
    const id = Number(itemEl.dataset.id);
    deleteTask(id);
    render();
  }
});
JavaScript

ここが中級ポイントです。

  • DOM の構造(closest など)を使って、「どのタスクの要素か」を見つける
  • data-id から id を取り出して、state を更新
  • そのあと render で再描画

「DOM → id → state」という経路を辿っていることを意識してください。


5 日目:フィルタボタンと「状態と UI の同期」

フィルタ状態を変更するロジック

フィルタは単純で、state.filter を変えるだけです。

function setFilter(filter) {
  state.filter = filter;
}
JavaScript

ただし、値は “all” / “todo” / “done” のみとします。

フィルタボタンにイベントを付ける

HTML 側で、ボタンに data-filter 属性を付けているので、それを使います。

filterButtons.forEach((button) => {
  button.addEventListener("click", () => {
    const filter = button.dataset.filter;
    setFilter(filter);
    render();
  });
});
JavaScript

ボタンの active クラスを切り替えるのが「UI 側の状態」です。
これも render の中で state.filter から決めるようにします。

function updateFilterButtons() {
  filterButtons.forEach((button) => {
    const filter = button.dataset.filter;
    if (filter === state.filter) {
      button.classList.add("active");
    } else {
      button.classList.remove("active");
    }
  });
}
JavaScript

ここでまた「状態(state.filter)から UI を決める」パターンが出てきました。

  • フィルタボタンを押す
  • state.filter を更新する
  • render() が呼ばれ、updateFilterButtons() の中で active クラスが付け替わる

ボタン自体に「自分が active かどうか」を記憶させない。
あくまで state を見て「あなたは active / inactive」と決める。
この一方向の流れが、設計をシンプルにしてくれます。


6 日目:コード全体を整理して「役割」を明確にする

関数の役割を言葉にする(重要)

ここまでで増えてきた関数を、役割別に整理します。

状態を変える関数(state を書き換える側):

  • addTask(title)
  • toggleTaskCompleted(id)
  • deleteTask(id)
  • setFilter(filter)

状態から UI を作る関数(state を読むだけの側):

  • render()
  • getFilteredTasks()
  • updateFilterButtons()

イベントを受けて橋渡しをする関数(UI と状態をつなぐ):

  • addButton / input / list / filterButtons に付けたイベントハンドラたち

この 3 層を意識しておくと、
「状態をいじるロジックが UI 側に紛れ込んでないか」
「DOM 操作が state 操作の中に紛れ込んでないか」
というチェックができるようになります。

初期データを入れて動作確認

テストのために、最初からいくつかタスクを入れておくと確認しやすいです。

state.tasks = [
  { id: nextId++, title: "サンプルタスク1", completed: false },
  { id: nextId++, title: "サンプルタスク2", completed: true },
];

render();
JavaScript

この状態で、

  • 追加
  • 完了切り替え
  • 削除
  • フィルタ切り替え

を一通り試し、挙動がおかしいところを見つけたら
「state がどうなっていて、それを render がどう解釈しているか」
という観点でデバッグしてみてください。


7 日目:設計の観点で振り返りと応用の入り口

この ToDo アプリの設計を言語化する

今回の ToDo アプリは、ざっくり言うとこういう設計です。

  • 真実の情報は state にだけ持つ(tasks 配列と filter 文字列)
  • ユーザー操作は、必ず「state を変える関数」を通る
  • state が変わったら、必ず render() で UI を描き直す
  • render は「state を読むだけ」であって、state をいじらない
  • フィルタは「表示ロジック(getFilteredTasks)」でのみ適用する

これをもっと抽象的に言うと、

  • 状態(model)
  • 描画(view)
  • イベント処理(controller)

のような役割に分かれている、と見ることもできます。
MVC や Flux、React の「単方向データフロー」などの土台になっている考え方です。

応用アイデア:設計を崩さずに機能を足す

例えば次のような機能を追加したくなったとします。

  • 期限(dueDate)を追加したい
  • タスクに「重要フラグ」を付けたい
  • 「未完了の件数」をヘッダーに表示したい

今回の設計なら、どれも次のように拡張できます。

期限を付けるなら:

  • Task の構造に dueDate フィールドを追加
  • addTask で dueDate を受け取る
  • render の中で表示を追加
  • フィルタやソート条件に dueDate を使いたければ、getFilteredTasks を拡張

重要フラグなら:

  • Task に isImportant を追加
  • toggleImportant(id) のような state 更新関数を作る
  • render で重要タスクにマークを付けて表示

どの場合も、

  • state の構造を変える
  • state を変える関数を足す
  • state から描画するロジックを足す

という流れで済むはずです。
逆に「DOM に直接フラグを持たせてしまう」設計にしていたら、この拡張は一気に苦しくなります。


まとめ:この 7 日間で鍛えた「設計の筋肉」

この ToDo アプリ【設計力強化編】で、あなたは次の感覚を手に入れています。

  • アプリの「状態(state)」をデータ構造として設計する
  • タスクを「配列+オブジェクト」で表現し、id で操作する
  • 「状態を書き換える関数」と「状態から UI を作る関数」をきっちり分ける
  • 「状態が変わったら render で全部描き直す」という単純で強いパターンを使える
  • フィルタを「データを壊さず表示だけ変える」ロジックとして設計する

これは、そのまま React や Vue を学ぶときに
「なんで state を一元管理するのか」「なんでコンポーネントは props から描画するのか」
を理解する土台になります。

タイトルとURLをコピーしました