+
onToggle(todo.id)}
+ className="mr-3 h-5 w-5 cursor-pointer"
+ />
- {/* 入力フォーム */}
-
+ {isEditing ? (
+
setEditText(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') handleSave();
+ if (e.key === 'Escape') handleCancel();
+ }}
+ className="flex-1 border border-blue-500 p-2 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
+ autoFocus
+ />
+ ) : (
+
setIsEditing(true)}
+ className={`flex-1 cursor-pointer p-2 rounded hover:bg-gray-200 transition-colors ${
+ todo.completed ? 'line-through text-gray-500' : 'text-gray-800'
+ }`}
+ >
+ {todo.text}
+
+ )}
+
+ {isEditing ? (
+
+
+
+
+ ) : (
+
+
+
+
+ )}
+
+ );
+}
+
+// src/app/components/TodoList.tsx
+interface TodoListProps {
+ todos: Todo[];
+ onToggle: (id: number) => void;
+ onDelete: (id: number) => void;
+ onEdit: (id: number, text: string) => void;
+}
+
+export function TodoList({ todos, onToggle, onDelete, onEdit }: TodoListProps) {
+ return (
+
+ {todos.length === 0 ? (
+
TODOがありません
+ ) : (
+ todos.map((todo) => (
+
+ ))
+ )}
+
+ );
+}
+
+// src/app/components/AddTodoForm.tsx
+interface AddTodoFormProps {
+ onAdd: (text: string) => void;
+}
+
+export function AddTodoForm({ onAdd }: AddTodoFormProps) {
+ const [inputValue, setInputValue] = useState('');
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (inputValue.trim() === '') {
+ alert('TODOを入力してください');
+ return;
+ }
+ onAdd(inputValue);
+ setInputValue('');
+ };
+
+ return (
+
+ );
+}
+
+// メインコンポーネント
+import { AddTodoForm } from './components/AddTodoForm';
+import { TodoList } from './components/TodoList';
+
+export default function Home() {
+ const [todos, setTodos] = useState
([]);
+
+ const addTodo = (text: string) => {
+ const newTodo: Todo = {
+ id: Date.now(),
+ text,
+ completed: false
+ };
+ setTodos([...todos, newTodo]);
+ };
+
+ const toggleTodo = (id: number) => {
+ setTodos(todos.map(todo =>
+ todo.id === id ? { ...todo, completed: !todo.completed } : todo
+ ));
+ };
+
+ const deleteTodo = (id: number) => {
+ setTodos(todos.filter(todo => todo.id !== id));
+ };
+
+ const editTodo = (id: number, text: string) => {
+ setTodos(todos.map(todo =>
+ todo.id === id ? { ...todo, text } : todo
+ ));
+ };
+
+ return (
+
+
+
+ );
+}
+```
+
+**コンポーネント分割のメリット:**
+
+- 単一責任の原則に従う
+- 再利用性が高まる
+- テストが書きやすくなる
+- コードが読みやすくなる
+
+**📚 このステップの参考文献:**
+
+- [React公式 - コンポーネントの分割](https://ja.react.dev/learn/thinking-in-react)
+- [コンポーネント設計のベストプラクティス](https://zenn.dev/yoshiko/articles/99f8047555f700)
+
+#### Step 5.2: エラーハンドリング
+
+**学習内容:**
+
+- 入力検証の強化
+- エラーメッセージの表示
+- エラーバウンダリーの実装
+
+**💡 詳細な実装と解説:**
+
+```typescript
+// src/app/components/ErrorBoundary.tsx
+'use client';
+
+import { Component, ErrorInfo, ReactNode } from 'react';
+
+interface Props {
+ children: ReactNode;
+}
+
+interface State {
+ hasError: boolean;
+ error?: Error;
+}
+
+export class ErrorBoundary extends Component {
+ public state: State = {
+ hasError: false
+ };
+
+ public static getDerivedStateFromError(error: Error): State {
+ return { hasError: true, error };
+ }
+
+ public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
+ console.error('エラーが発生しました:', error, errorInfo);
+ }
+
+ public render() {
+ if (this.state.hasError) {
+ return (
+
+
+
エラーが発生しました
+
+ 申し訳ございません。予期しないエラーが発生しました。
+
+
+
+
+ );
+ }
+
+ return this.props.children;
+ }
+}
+
+// カスタムフックでエラー処理
+const useErrorHandler = () => {
+ const [error, setError] = useState(null);
+
+ const handleError = (errorMessage: string) => {
+ setError(errorMessage);
+ setTimeout(() => setError(null), 5000); // 5秒後に自動で消す
+ };
+
+ const clearError = () => setError(null);
+
+ return { error, handleError, clearError };
+};
+
+// エラーメッセージコンポーネント
+const ErrorMessage = ({ message, onClose }: { message: string; onClose: () => void }) => (
+
+ {message}
+
+
+);
+
+// 入力検証の強化
+const validateTodo = (text: string): string | null => {
+ if (text.trim() === '') {
+ return 'TODOを入力してください';
+ }
+ if (text.length > 100) {
+ return 'TODOは100文字以内で入力してください';
+ }
+ if (text.trim().length < 2) {
+ return 'TODOは2文字以上で入力してください';
+ }
+ return null;
+};
+
+// 使用例
+export function AddTodoForm({ onAdd }: AddTodoFormProps) {
+ const [inputValue, setInputValue] = useState('');
+ const { error, handleError, clearError } = useErrorHandler();
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ clearError();
+
+ const validationError = validateTodo(inputValue);
+ if (validationError) {
+ handleError(validationError);
+ return;
+ }
+
+ try {
+ onAdd(inputValue);
+ setInputValue('');
+ } catch (err) {
+ handleError('TODOの追加に失敗しました');
+ }
+ };
+
+ return (
+
+ {error && }
+
-
- {/* TODOリスト表示 */}
-
- {todos.map((todo) => (
- -
- {todo.text}
-
- ))}
-
-
+
+
+
);
}
```
-**解説:**
+**📚 このステップの参考文献:**
-- `'use client'` - クライアントサイドで実行するコンポーネントであることを宣言
-- `useState