
今日のゴール
- 関数を小さく分ける設計感覚をつかむ
- map / filter / reduce の「雰囲気+使いどころ」を体感
- fetch でAPIからJSONを取り、画面に表示
- 1日目のカウンターを発展させて、ToDoアプリを完成(localStorage保存)
Contents
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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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操作(副作用)を集約し、配列メソッドで組み立てた結果を一括で描画。
- 保存は
localStorageに JSON文字列で(読み込み時はJSON.parse)。
5. つまずきメモ(2日目)
- 何も起きない → Consoleにエラーがないか? 関数が実行されているか
console.log()を刺す。 - 表示が古い →
render()が呼ばれているか確認。配列を書き換えたら必ず render。 - 保存されない → ブラウザを変えると
localStorageは別です。同じブラウザで確認。
6. まとめ & 次回(3日目予告)
- 関数分解で「入力→出力」をはっきりさせると、配列メソッドをパズルのように組める。
- map/filter/reduce はUIの下ごしらえに最高。
- fetch で外の世界とつながると、一気にアプリ感が出る。
- ToDo は「配列+レンダ+保存」の練習に最適。
3日目は:
fetchのエラーハンドリングとリトライ- **モジュール化(ES Modules)**とファイル分割
- フォームのバリデーション&軽いテストの考え方(
console.assertなど)









