📚

React×laravel Todoリスト(2)

2024/10/21に公開

CRUD機能すべて実装したので一応メモに

まずlaravel側のcontller

TodoController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Todo;

class TodoController extends Controller
{
    // 新しいTodoを追加するメソッド
    public function setTodo(Request $request)
    {
        // リクエストのバリデーション: 'content' フィールドが必須であり、文字列で、最大255文字までであることを検証
        $request->validate([
            'content' => 'required|string|max:255'
        ]);

        // Todoを作成してデータベースに保存
        $todo = Todo::create([
            'content' => $request->content
        ]);

        // 作成したTodoをJSON形式で返す
        return response()->json($todo, 201); // ステータスコード201で返す
    }

    // 全てのTodoを取得して返すメソッド
    public function index()
    {
        // Todoのすべてのレコードを取得して変数に格納
        $todos = Todo::all();

        // 取得したTodoのリストをJSON形式で返す
        return response()->json($todos);
    }

    public function deleteTodo($id){

        $todo = Todo::find($id);

        if($todo){
            $todo->delete();
        }

    }

    // Todoのcontentを更新するメソッド
    public function updateTodo(Request $request, $id)
    {
        // リクエストのバリデーション: 'content' フィールドが必須であり、文字列で、最大255文字までであることを検証
        $request->validate([
            'content' => 'required|string|max:255'
        ]);

        // 指定されたIDのTodoを取得
        $todo = Todo::find($id);

        if ($todo) {
            // Todoのcontentを更新
            $todo->content = $request->content;
            $todo->save(); // 更新をデータベースに保存

            // 更新したTodoをJSON形式で返す
            return response()->json($todo, 200); // ステータスコード200で返す
        }

        // Todoが見つからない場合は404を返す
        return response()->json(['message' => 'Todo not found'], 404);
    }


}

モデルは変更なしですが一応

Todo.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Todo extends Model
{
    use HasFactory;
    // $fillable に定義されたフィールドは、ユーザーがリクエストを通してデータをモデルに一括で割り当てる際に、保存できる属性のリストです。
    // $fillable を使うことで、セキュリティの確保を目的としています。リクエストから予期しないデータが送信されても、許可されたフィールドのみをデータベースに保存できるように制限することで、マスアサインメント脆弱性を防ぎます。
    protected $fillable = ['content'];
}

laravel側のapiエンドポイント

web.php
<?php

use App\Http\Controllers\ProfileController;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\TodoController;


// Todo APIエンドポイント
Route::post('/api/todo', [TodoController::class, 'setTodo']);
Route::get('/api/todo', [TodoController::class, 'index']);
Route::delete('/api/todo/{id}', [TodoController::class, 'deleteTodo']);
Route::put('api/todo/{id}', [TodoController::class, 'updateTodo']);


// フロントエンドのすべてのリクエストをキャッチするルート
Route::get('{any}', function () {
    return view('app');
})->where('any','.*');

フロント側Layouts

TodoLayouts.tsx
import CommonHeader from "@/Components/Lv2/CommonHeader";
import ShowTodoList from "@/Components/Lv3/ShowTodoList";
import TodoForm from "@/Components/Lv3/TodoForm";
import axios from "axios";
import React, { useEffect, useState } from "react";

const TodoLayouts: React.FC = () => {
    const [todos, setTodos] = useState<{ id: number; content: string }[]>([]);
    const [errorMessage, setErrorMessage] = useState("");

    // Todoリストの取得
    const fetchTodos = async () => {
        try {
            const response = await axios.get("/api/todo");
            setTodos(response.data);
        } catch (error) {
            console.error("Error fetching todos:", error);
            setErrorMessage("Todoリストの取得に失敗しました");
        }
    };

    // Todoリストの追加
    const addTodo = async (content: string) => {
        try {
            await axios.post("/api/todo", { content });
            fetchTodos(); // 追加後に再取得
        } catch (error) {
            console.error("Error adding todo:", error);
            setErrorMessage("Todoの追加に失敗しました");
        }
    };

    // Todoリストの削除
    const deleteTodo = async (id: number) => {
        try {
            await axios.delete(`/api/todo/${id}`);
            fetchTodos(); // 削除後に再取得
        } catch (error) {
            console.error("Error deleting todo:", error);
            setErrorMessage("Todoの削除に失敗しました");
        }
    };

    // Todoリストの更新
    const updateTodo = async (id: number, newContent: string) => {
        try {
            await axios.put(`/api/todo/${id}`, { content: newContent });
            fetchTodos(); // 更新後に再取得
        } catch (error) {
            console.error("Error updating todo:", error);
            setErrorMessage("Todoの更新に失敗しました");
        }
    };

    useEffect(() => {
        // 初回ロード時にTodoリストを取得
        fetchTodos();
    }, []);

    return (
        <>
            <CommonHeader />
            {errorMessage && <p style={{ color: "red" }}>{errorMessage}</p>}
            <TodoForm onAddTodo={(content) => addTodo(content)} />
            <ShowTodoList
                todos={todos}
                onDeleteTodo={deleteTodo}
                onUpdateTodo={updateTodo}
            />
        </>
    );
};

export default TodoLayouts;

入力部分コンポーネント

TodoForm.tsx
import React, { useState } from "react";
import { Box, TextField, Typography } from "@mui/material";
import Button from "../Lv1/ButtonAtom";

interface TodoFormProps {
    onAddTodo: (content: string) => void;
}

const TodoForm: React.FC<TodoFormProps> = ({ onAddTodo }) => {
    const [content, setContent] = useState("");
    const [message, setMessage] = useState("");

    const handleSubmit = (e: React.FormEvent) => {
        e.preventDefault();
        if (content.trim() === "") {
            setMessage("内容を入力してください");
            return;
        }

        // 親コンポーネントのonAddTodo関数を呼び出す
        onAddTodo(content);
        setMessage(content + "を追加しました");
        setContent("");
    };

    return (
        <Box sx={{ maxWidth: 400, mx: "auto", mt: 4 }}>
            <Typography variant="h4" component="h1" gutterBottom>
                やりたいことリスト
            </Typography>
            <form onSubmit={handleSubmit}>
                <TextField
                    label="入力してください"
                    value={content}
                    onChange={(e) => setContent(e.target.value)}
                    fullWidth
                    margin="normal"
                    required
                />
                <Button type="submit" visual="primary" size="medium">
                    目標を送信
                </Button>
            </form>
            {message && (
                <Typography variant="body2" color="textSecondary">
                    {message}
                </Typography>
            )}
        </Box>
    );
};

export default TodoForm;

表示部分のコンポーネント

ShowTodoList
import React, { useState } from "react";
import {
    Box,
    List,
    ListItem,
    ListItemText,
    Typography,
    TextField,
} from "@mui/material";
import Button from "../Lv1/ButtonAtom";

interface ShowTodoListProps {
    todos: { id: number; content: string }[];
    onDeleteTodo: (id: number) => void; // 削除用のコールバック関数
    onUpdateTodo: (id: number, newContent: string) => void; // 更新用のコールバック関数
}

const ShowTodoList: React.FC<ShowTodoListProps> = ({
    todos,
    onDeleteTodo,
    onUpdateTodo,
}) => {
    const [editingTodoId, setEditingTodoId] = useState<number | null>(null);
    const [newContent, setNewContent] = useState("");

    return (
        <Box sx={{ maxWidth: 400, mx: "auto", mt: 4 }}>
            <Typography variant="h4" component="h1" gutterBottom>
                Item List
            </Typography>
            <List>
                {todos.map((todo) => (
                    <ListItem
                        key={todo.id}
                        sx={{ border: "2px solid #ccc", alignItems: "center" }}
                    >
                        <ListItemText primary={`ID: ${todo.id}`} />
                        {editingTodoId === todo.id ? (
                            <>
                                <TextField
                                    variant="outlined"
                                    value={newContent}
                                    onChange={(e) =>
                                        setNewContent(e.target.value)
                                    }
                                    placeholder="新しい内容を入力"
                                    size="small"
                                />
                                <Button
                                    type="button"
                                    visual="primary"
                                    size="medium"
                                    onClick={() => {
                                        onUpdateTodo(todo.id, newContent);
                                        setEditingTodoId(null);
                                    }}
                                >
                                    更新
                                </Button>
                                <Button
                                    type="button"
                                    visual="secondary"
                                    size="medium"
                                    onClick={() => setEditingTodoId(null)}
                                >
                                    キャンセル
                                </Button>
                            </>
                        ) : (
                            <>
                                <ListItemText primary={todo.content} />
                                <Button
                                    type="button"
                                    visual="primary"
                                    size="medium"
                                    onClick={() => {
                                        setEditingTodoId(todo.id);
                                        setNewContent(todo.content);
                                    }}
                                >
                                    変更
                                </Button>
                                <Button
                                    type="button"
                                    visual="alert"
                                    size="medium"
                                    onClick={() => onDeleteTodo(todo.id)}
                                >
                                    削除
                                </Button>
                            </>
                        )}
                    </ListItem>
                ))}
            </List>
        </Box>
    );
};

export default ShowTodoList;

このぐらいの規模なら大丈夫だが、改善点としてはlaravelの方
controllerですべての処理を行っているので、
リポジトリ、サービスなどに分けた方が実践的かなと考えました。
フロント側での改善点はapiを別で分けた方が良かった。use〇〇とかで非同期処理を行うカスタムフックを

Discussion