📚
React×laravel Todoリスト(2)
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