JavaScript勉強 2日目:小さく分けて、配列を味方に、そしてデータを取る

今日のゴール

  1. 関数を小さく分ける設計感覚をつかむ
  2. map / filter / reduce の「雰囲気+使いどころ」を体感
  3. fetch でAPIからJSONを取り、画面に表示
  4. 1日目のカウンターを発展させて、ToDoアプリを完成(localStorage保存)

1. 関数は「小さく」「役割ひとつ」

まずは“その関数、何を入力して何を返す?”を意識します。

  • UI更新(副作用)は一箇所に集める
  • 計算や配列加工は引数→返り値純粋関数にする
// 悪い例:何でもかんでも1つの関数でやる(読みづらい・テストしづらい)

// 良い例:役割を分ける
function formatUser(u) {
  return `${u.id}: ${u.name}`;
}
function renderList(container, items) {
  container.innerHTML = items.map(i => `<li>${i}</li>`).join('');
}

// 使う側
const users = [{id:1,name:'Hanako'},{id:2,name:'Taro'}];
const lines = users.map(formatUser);     // 純粋関数の組み合わせ
const ul = document.querySelector('#list');
renderList(ul, lines);                   // DOM更新は一箇所

2. 配列メソッド:map / filter / reduce

  • map:配列の形を変える(同じ長さ)。
  • filter:条件で間引く
  • reduce:配列を1つの値に畳み込む(合計・集計・オブジェクト化など)。
const todos = [
  { id:1, title:'牛乳を買う', done:false },
  { id:2, title:'メール返信', done:true },
  { id:3, title:'散歩',     done:false },
];

// map: タイトルだけの配列へ
const titles = todos.map(t => t.title); 
// filter: 未完了だけ
const active = todos.filter(t => !t.done);
// reduce: 未完了数をカウント
const activeCount = todos.reduce((acc, t) => acc + (t.done ? 0 : 1), 0);

ポイント:map/filterは配列を返すので、そのまま連結しやすい。
todos.filter(...).map(...) など“パイプライン”で考えると気持ちいいです。


3. fetchでAPIからJSONを取って表示(async/await)

fetch → レスポンスをJSONに → 配列をmapしてHTML化 → DOM更新」の流れを体験します。
(通信失敗もあり得るので、try/catchロード中表示を忘れずに)

<!-- index.html の一部(API表示デモ) -->
<section class="card">
  <h2>APIからユーザー一覧(サンプル)</h2>
  <button id="loadUsers">読み込む</button>
  <ul id="users"></ul>
  <p id="usersStatus" class="muted"></p>
</section>

<script>
const usersUl = document.querySelector('#users');
const usersStatus = document.querySelector('#usersStatus');

document.querySelector('#loadUsers').addEventListener('click', loadUsers);

async function loadUsers() {
  usersStatus.textContent = '読み込み中...';
  usersUl.innerHTML = '';

  try {
    const res = await fetch('https://jsonplaceholder.typicode.com/users?_limit=5');
    if (!res.ok) throw new Error('HTTP ' + res.status);
    const data = await res.json(); // ← 配列

    // mapで<li>化 → joinで文字列に → innerHTMLへ
    usersUl.innerHTML = data
      .map(u => `<li>${u.id}. ${u.name} <small>(${u.email})</small></li>`)
      .join('');

    usersStatus.textContent = `読み込み成功(${data.length}件)`;
  } catch (err) {
    console.error(err);
    usersStatus.textContent = '読み込みに失敗しました';
  }
}
</script>

4. かんたんToDoアプリ(保存は localStorage)

できること

  • 追加 / 完了トグル / 削除
  • 絞り込み(すべて / 未完了 / 完了)
  • 件数カウント
  • ブラウザに自動保存(localStorage)

完成コード(1ファイル・コピペで動きます)

todo.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <title>ToDo(JS 2日目)</title>
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <style>
    body { font-family: system-ui, sans-serif; max-width: 720px; margin: 40px auto; line-height: 1.8; }
    .card { background:#fafafa; border:1px solid #eee; border-radius:12px; padding:16px; margin:12px 0; }
    .row { display:flex; gap:8px; }
    input[type="text"]{ flex:1; padding:8px 10px; border-radius:8px; border:1px solid #ddd; }
    button { padding:8px 12px; border:1px solid #ddd; border-radius:8px; cursor:pointer; background:white; }
    ul { list-style:none; padding:0; margin:0; }
    li { display:flex; align-items:center; gap:8px; border-bottom:1px solid #eee; padding:8px 0; }
    li.done .title{ text-decoration: line-through; color:#888; }
    .title { flex:1; }
    .muted { color:#777; font-size:0.9rem; }
    .filters { display:flex; gap:8px; margin-top:8px; }
    .filters button.active { background:#111; color:#fff; }
    .right { margin-left:auto; }
  </style>
</head>
<body>
  <h1>ToDoアプリ(2日目)</h1>

  <section class="card">
    <div class="row">
      <input id="todoInput" placeholder="やることを入力してEnter..." />
      <button id="addBtn">追加</button>
    </div>
    <div class="filters">
      <button data-filter="all" class="active">すべて</button>
      <button data-filter="active">未完了</button>
      <button data-filter="done">完了</button>
      <span class="right muted">未完了: <span id="leftCount">0</span> 件</span>
    </div>
  </section>

  <section class="card">
    <ul id="todoList"></ul>
    <div class="row" style="margin-top:8px;">
      <button id="clearDone">完了を一括削除</button>
      <span class="right muted">合計: <span id="totalCount">0</span> 件</span>
    </div>
  </section>

<script>
/** ===== 状態と永続化 ===== **/
const STORAGE_KEY = 'js-day2-todos';
let todos = load() || [];
let filter = 'all'; // 'all' | 'active' | 'done'

function save(){ localStorage.setItem(STORAGE_KEY, JSON.stringify(todos)); }
function load(){
  try { return JSON.parse(localStorage.getItem(STORAGE_KEY)); }
  catch { return []; }
}

/** ===== 純粋関数(配列操作) ===== **/
function createTodo(title){
  return { id: Date.now() + Math.random().toString(16).slice(2), title, done:false };
}
function toggleById(list, id){
  return list.map(t => t.id === id ? { ...t, done: !t.done } : t);
}
function removeById(list, id){
  return list.filter(t => t.id !== id);
}
function clearDone(list){
  return list.filter(t => !t.done);
}
function filterTodos(list, mode){
  if(mode === 'active') return list.filter(t => !t.done);
  if(mode === 'done')   return list.filter(t =>  t.done);
  return list;
}
function leftCount(list){
  return list.reduce((acc, t) => acc + (t.done ? 0 : 1), 0);
}

/** ===== DOM参照 ===== **/
const input = document.querySelector('#todoInput');
const addBtn = document.querySelector('#addBtn');
const listEl = document.querySelector('#todoList');
const leftCountEl = document.querySelector('#leftCount');
const totalCountEl = document.querySelector('#totalCount');
const filterBtns = document.querySelectorAll('.filters button');
const clearDoneBtn = document.querySelector('#clearDone');

/** ===== レンダリング(副作用まとめ) ===== **/
function render(){
  // フィルタ適用後の配列でLIを作る
  const show = filterTodos(todos, filter);
  listEl.innerHTML = show.map(t => `
    <li class="${t.done ? 'done' : ''}" data-id="${t.id}">
      <input type="checkbox" class="toggle" ${t.done ? 'checked' : ''}/>
      <span class="title">${escapeHTML(t.title)}</span>
      <button class="del">削除</button>
    </li>
  `).join('');

  // 件数
  leftCountEl.textContent = leftCount(todos);
  totalCountEl.textContent = todos.length;

  // フィルタボタンの見た目
  filterBtns.forEach(b => b.classList.toggle('active', b.dataset.filter === filter));

  save(); // 表示のたびに保存(簡単のため)
}

// XSS対策の超簡易エスケープ
function escapeHTML(str) {
  return str.replace(/[&<>"']/g, s => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[s]));
}

/** ===== イベント ===== **/
// 追加
addBtn.addEventListener('click', () => tryAdd());
input.addEventListener('keydown', (e) => { if(e.key === 'Enter') tryAdd(); });

function tryAdd(){
  const title = input.value.trim();
  if(!title) return;
  todos = [createTodo(title), ...todos];
  input.value = '';
  render();
}

// チェック/削除(イベント委譲)
listEl.addEventListener('click', (e) => {
  const li = e.target.closest('li');
  if(!li) return;
  const id = li.dataset.id;

  if(e.target.classList.contains('toggle')){
    todos = toggleById(todos, id);
    render();
  }
  if(e.target.classList.contains('del')){
    todos = removeById(todos, id);
    render();
  }
});

// フィルタ切替
filterBtns.forEach(btn => {
  btn.addEventListener('click', () => {
    filter = btn.dataset.filter;
    render();
  });
});

// 完了一括削除
clearDoneBtn.addEventListener('click', () => {
  if(!todos.some(t => t.done)) return;
  if(confirm('完了したToDoをすべて削除しますか?')){
    todos = clearDone(todos);
    render();
  }
});

// 初回表示
render();
</script>
</body>
</html>

解説のキモ

  • 純粋関数createTodo, toggleById, removeById など)は配列を受けて、新しい配列を返すだけ。
  • render()DOM操作(副作用)を集約し、配列メソッドで組み立てた結果を一括で描画
  • 保存は localStorageJSON文字列で(読み込み時は JSON.parse)。

5. つまずきメモ(2日目)

  • 何も起きない → Consoleにエラーがないか? 関数が実行されているか console.log() を刺す。
  • 表示が古いrender() が呼ばれているか確認。配列を書き換えたら必ず render
  • 保存されない → ブラウザを変えると localStorage は別です。同じブラウザで確認。

6. まとめ & 次回(3日目予告)

  • 関数分解で「入力→出力」をはっきりさせると、配列メソッドをパズルのように組める
  • map/filter/reduce はUIの下ごしらえに最高。
  • fetch で外の世界とつながると、一気にアプリ感が出る。
  • ToDo は「配列+レンダ+保存」の練習に最適。

3日目は:

  • fetch のエラーハンドリングとリトライ
  • **モジュール化(ES Modules)**とファイル分割
  • フォームのバリデーション&軽いテストの考え方(console.assert など)
おすすめの記事