step5-3 最適化まで終了

This commit is contained in:
2025-09-29 09:06:45 +09:00
parent d387a00dff
commit f7d9950bbc
4 changed files with 116 additions and 111 deletions

View File

@@ -1,6 +1,6 @@
'use client';
import { useState, useEffect } from 'react';
import { useState, useEffect, useCallback, useMemo } from 'react';
import TodoList from '@/components/TodoList';
import FilterButtons from '@/components/FilterButtons';
@@ -37,25 +37,27 @@ export default function Home() {
const [error, setError] = useState<AppError | null>(null);
const [showErrorDialog, setShowErrorDialog] = useState(false);
// フィルターに応じたTodoの数をカウント
const activeCount = todos.filter(todo => !todo.completed).length;
const completedCount = todos.filter(todo => todo.completed).length;
// フィルターに応じたTodoの数をメモ化
const { activeCount, completedCount } = useMemo(() => ({
activeCount: todos.filter(todo => !todo.completed).length,
completedCount: todos.filter(todo => todo.completed).length,
}), [todos]);
// エラー処理用のヘルパー関数
const handleError = (error: AppError) => {
// エラー処理用のヘルパー関数をメモ化
const handleError = useCallback((error: AppError) => {
console.error('アプリケーションエラー:', error);
setError(error);
setShowErrorDialog(true);
};
}, []);
// エラーダイアログを閉じる
const closeErrorDialog = () => {
const closeErrorDialog = useCallback(() => {
setShowErrorDialog(false);
setError(null);
};
}, []);
// バリデーション関数
const validateTodoText = (text: string): boolean => {
// バリデーション関数をメモ化
const validateTodoText = useCallback((text: string): boolean => {
if (!text || text.trim().length === 0) {
handleError({
type: 'VALIDATION_ERROR',
@@ -73,10 +75,10 @@ export default function Home() {
}
return true;
};
}, [handleError]);
// Local Storage操作をtry-catchで包む
const safeLocalStorageGet = (key: string): string | null => {
// Local Storage操作をメモ化
const safeLocalStorageGet = useCallback((key: string): string | null => {
try {
return localStorage.getItem(key);
} catch (error) {
@@ -86,9 +88,9 @@ export default function Home() {
});
return null;
}
};
}, [handleError]);
const safeLocalStorageSet = (key: string, value: string): boolean => {
const safeLocalStorageSet = useCallback((key: string, value: string): boolean => {
try {
localStorage.setItem(key, value);
return true;
@@ -99,14 +101,15 @@ export default function Home() {
});
return false;
}
};
}, [handleError]);
const startEdit = (todo: Todo) => {
// 編集関連の関数をメモ化
const startEdit = useCallback((todo: Todo) => {
setEditingId(todo.id);
setEditText(todo.text);
};
}, []);
const saveEdit = () => {
const saveEdit = useCallback(() => {
if (!validateTodoText(editText)) {
return;
}
@@ -128,10 +131,15 @@ export default function Home() {
message: 'TODOの更新に失敗しました'
});
}
};
}, [editText, editingId, todos, validateTodoText, handleError]);
// 新しいTODOを追加する関数エラーハンドリング追加
const addTodo = () => {
const cancelEdit = useCallback(() => {
setEditingId(null);
setEditText('');
}, []);
// TODO操作の関数をメモ化
const addTodo = useCallback(() => {
if (!validateTodoText(inputValue)) {
return;
}
@@ -142,7 +150,7 @@ export default function Home() {
text: inputValue.trim(),
completed: false
};
setTodos([...todos, newTodo]);
setTodos(prev => [...prev, newTodo]);
setInputValue('');
} catch (error) {
handleError({
@@ -150,48 +158,38 @@ export default function Home() {
message: 'TODOの追加に失敗しました'
});
}
};
}, [inputValue, validateTodoText, handleError]);
const cancelEdit = () => {
setEditingId(null);
setEditText('');
};
// フォーム送信時の処理
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
const handleSubmit = useCallback((e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
addTodo();
};
}, [addTodo]);
// Todoの完了状態をトグルする関数
const toggleTodo = (id: number) => {
const toggleTodo = useCallback((id: number) => {
try {
const updatedTodos = todos.map(todo => {
setTodos(prev => prev.map(todo => {
if (todo.id === id) {
return { ...todo, completed: !todo.completed };
}
return todo;
});
setTodos(updatedTodos);
}));
} catch (error) {
handleError({
type: 'VALIDATION_ERROR',
message: 'TODO状態の更新に失敗しました'
});
}
};
}, [handleError]);
// 削除ボタンが押されたときの処理(確認ダイアログ表示)
const handleDeleteClick = (id: number) => {
const handleDeleteClick = useCallback((id: number) => {
setShowConfirm(true);
setTargetId(id);
};
}, []);
// ダイアログで「はい」が押されたときの本削除処理
const confirmDelete = () => {
const confirmDelete = useCallback(() => {
if (targetId !== null) {
try {
setTodos(todos.filter(todo => todo.id !== targetId));
setTodos(prev => prev.filter(todo => todo.id !== targetId));
setShowConfirm(false);
setTargetId(null);
} catch (error) {
@@ -201,64 +199,30 @@ export default function Home() {
});
}
}
};
}, [targetId, handleError]);
// ダイアログで「いいえ」が押されたときの処理
const cancelDelete = () => {
const cancelDelete = useCallback(() => {
setShowConfirm(false);
setTargetId(null);
};
}, []);
// 空欄入力時ダイアログを閉じる関数
const closeInputAlert = () => {
const closeInputAlert = useCallback(() => {
setShowInputAlert(false);
};
}, []);
// 完了済みTODOをすべて削除
const clearCompleted = () => {
const clearCompleted = useCallback(() => {
try {
setTodos(todos.filter(todo => !todo.completed));
setTodos(prev => prev.filter(todo => !todo.completed));
} catch (error) {
handleError({
type: 'VALIDATION_ERROR',
message: '完了済みTODOの削除に失敗しました'
});
}
};
}, [handleError]);
// アプリ起動時にデータを読み込み(エラーハンドリング強化)
useEffect(() => {
const savedTodos = safeLocalStorageGet(STORAGE_KEY);
if (savedTodos) {
try {
const parsedTodos = JSON.parse(savedTodos);
// データの妥当性をチェック
if (Array.isArray(parsedTodos)) {
const validTodos = parsedTodos.filter(todo =>
todo &&
typeof todo.id === 'number' &&
typeof todo.text === 'string' &&
typeof todo.completed === 'boolean'
);
setTodos(validTodos);
}
} catch (error) {
handleError({
type: 'STORAGE_ERROR',
message: '保存されたデータの形式が正しくありません'
});
}
}
}, []);
// TODOが変更されるたびに保存エラーハンドリング追加
useEffect(() => {
safeLocalStorageSet(STORAGE_KEY, JSON.stringify(todos));
}, [todos]);
// データのエクスポート機能(エラーハンドリング追加)
const exportData = () => {
// エクスポート・インポート機能をメモ化
const exportData = useCallback(() => {
try {
const dataStr = JSON.stringify(todos, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
@@ -274,10 +238,9 @@ export default function Home() {
message: 'データのエクスポートに失敗しました'
});
}
};
}, [todos, handleError]);
// データのインポート機能(エラーハンドリング強化)
const importData = (event: React.ChangeEvent<HTMLInputElement>) => {
const importData = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
const reader = new FileReader();
@@ -285,7 +248,6 @@ export default function Home() {
try {
const importedData = JSON.parse(e.target?.result as string);
// インポートデータの妥当性をチェック
if (!Array.isArray(importedData)) {
throw new Error('無効なデータ形式です');
}
@@ -302,8 +264,6 @@ export default function Home() {
}
setTodos(validTodos);
// ファイル入力をリセット
event.target.value = '';
} catch (error) {
handleError({
@@ -324,7 +284,41 @@ export default function Home() {
reader.readAsText(file);
}
};
}, [handleError]);
// アプリ起動時にデータを読み込み
useEffect(() => {
const savedTodos = safeLocalStorageGet(STORAGE_KEY);
if (savedTodos) {
try {
const parsedTodos = JSON.parse(savedTodos);
if (Array.isArray(parsedTodos)) {
const validTodos = parsedTodos.filter(todo =>
todo &&
typeof todo.id === 'number' &&
typeof todo.text === 'string' &&
typeof todo.completed === 'boolean'
);
setTodos(validTodos);
}
} catch (error) {
handleError({
type: 'STORAGE_ERROR',
message: '保存されたデータの形式が正しくありません'
});
}
}
}, [safeLocalStorageGet, handleError]);
// TODOが変更されるたびに保存デバウンス処理付き
useEffect(() => {
const timeoutId = setTimeout(() => {
safeLocalStorageSet(STORAGE_KEY, JSON.stringify(todos));
}, 300); // 300ms後に保存
return () => clearTimeout(timeoutId);
}, [todos, safeLocalStorageSet]);
return (
<main className="min-h-screen p-8 bg-gray-50">

View File

@@ -11,7 +11,7 @@ type FilterButtonsProps = {
onClearCompleted: () => void;
};
export default function FilterButtons({
const FilterButtons = React.memo(function FilterButtons({
filter,
totalCount,
activeCount,
@@ -64,4 +64,6 @@ export default function FilterButtons({
)}
</div>
);
}
});
export default FilterButtons;

View File

@@ -18,7 +18,7 @@ type TodoItemProps = {
onEditTextChange: (text: string) => void;
};
export default function TodoItem({
const TodoItem = React.memo(function TodoItem({
todo,
isEditing,
editText,
@@ -29,6 +29,11 @@ export default function TodoItem({
onDelete,
onEditTextChange,
}: TodoItemProps) {
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') onSaveEdit();
if (e.key === 'Escape') onCancelEdit();
};
return (
<div className="flex items-center p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
<input
@@ -37,16 +42,14 @@ export default function TodoItem({
onChange={onToggle}
className="mr-3 h-5 w-5 cursor-pointer"
/>
{isEditing ? (
// 編集モード
<input
type="text"
value={editText}
onChange={(e) => onEditTextChange(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') onSaveEdit();
if (e.key === 'Escape') onCancelEdit();
}}
onKeyDown={handleKeyDown}
className="flex-1 border border-blue-500 p-2 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
autoFocus
/>
@@ -61,6 +64,7 @@ export default function TodoItem({
{todo.text}
</span>
)}
{isEditing ? (
// 編集モードのボタン
<div className="flex gap-2 ml-2">
@@ -96,4 +100,6 @@ export default function TodoItem({
)}
</div>
);
}
});
export default TodoItem;

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useMemo } from 'react';
import TodoItem from './TodoItem';
type Todo = {
@@ -22,7 +22,7 @@ type TodoListProps = {
onEditTextChange: (text: string) => void;
};
export default function TodoList({
const TodoList = React.memo(function TodoList({
todos,
filter,
editingId,
@@ -34,7 +34,8 @@ export default function TodoList({
onDeleteClick,
onEditTextChange,
}: TodoListProps) {
const getFilteredTodos = () => {
// フィルタリング処理をメモ化
const filteredTodos = useMemo(() => {
switch (filter) {
case 'active':
return todos.filter(todo => !todo.completed);
@@ -43,18 +44,18 @@ export default function TodoList({
default:
return todos;
}
};
}, [todos, filter]);
return (
<div className="space-y-2">
{getFilteredTodos().length === 0 ? (
{filteredTodos.length === 0 ? (
<p className="text-gray-500 text-center py-8">
{filter === 'active' && 'すべて完了しました!'}
{filter === 'completed' && '完了したTODOがありません'}
{filter === 'all' && 'TODOがありません'}
</p>
) : (
getFilteredTodos().map((todo) => (
filteredTodos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
@@ -71,4 +72,6 @@ export default function TodoList({
)}
</div>
);
}
});
export default TodoList;