0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Remixを学びたい(Step3:アクションを実装する)

0
Last updated at Posted at 2025-04-29

はじめに

前回までの続きです。
今回はアクションを実装し、TODOの新規追加や更新、削除の実装をしていきます!

シリーズもの

成果物

step3.gif
→今回は新規追加や更新、削除の実装をしています

ソースコード

ディレクトリ構成
~/develop/remix_sample  (feat/action)$ tree app/
app/
├── actions
│   └── todoActions.ts
├── components
│   └── todo
│       ├── TodoForm.tsx # 登録フォーム
│       ├── TodoItem.tsx # 登録Item
│       └── TodoList.tsx  # 登録リスト
├── entry.client.tsx
├── entry.server.tsx
├── models
│   └── todo.ts
├── root.tsx
├── routes
│   └── _index.tsx # フロント
├── services
│   └── todoService.ts # バックエンド
└── tailwind.css

7 directories, 11 files

ソースコード

app/routes/_index.tsx
import { json, type ActionFunctionArgs } from "@remix-run/node";
import { useLoaderData, useNavigation } from "@remix-run/react";
import type { MetaFunction } from "@remix-run/node";
import { getTodos } from "~/services/todoService";
import { TodoForm } from "~/components/todo/TodoForm";
import { TodoList } from "~/components/todo/TodoList";
import {
	handleAddTodo,
	handleToggleTodo,
	handleDeleteTodo,
} from "~/actions/todoActions";

export const meta: MetaFunction = () => {
	return [
		{ title: "TODO App" },
		{ name: "description", content: "Simple TODO App with Remix" },
	];
};

// データを取得(サーバーサイド)
export async function loader() {
	const todos = await getTodos();
	return json({ todos });
}

// データを更新(サーバーサイド)
export async function action({ request }: ActionFunctionArgs) {
	const formData = await request.formData();
	const intent = formData.get("intent");

	switch (intent) {
		case "add":
			return handleAddTodo(formData);
		case "toggle":
			return handleToggleTodo(formData);
		case "delete":
			return handleDeleteTodo(formData);
		default:
			return json({ error: "Invalid intent" }, { status: 400 });
	}
}

export default function Index() {
	const { todos } = useLoaderData<typeof loader>();
	const navigation = useNavigation();
	const isLoading = navigation.state === "loading";

	return (
		<div className="max-w-2xl mx-auto p-4">
			<h1 className="text-2xl font-bold mb-4">TODO App</h1>

			{isLoading ? (
				<div className="text-center text-gray-500 my-4">Loading...</div>
			) : (
				<>
					<TodoForm />
					<TodoList todos={todos} />
				</>
			)}
		</div>
	);
}
app/actions/todoActions.ts
import { json } from "@remix-run/node";
import { addTodo, toggleTodo, deleteTodo } from "~/services/todoService";

export async function handleAddTodo(formData: FormData) {
	const title = formData.get("title");
	if (typeof title !== "string" || !title) {
		return json({ error: "Title is required" }, { status: 400 });
	}
	await addTodo(title);
	return json({ ok: true });
}

export async function handleToggleTodo(formData: FormData) {
	const id = formData.get("id");
	if (typeof id === "string") {
		await toggleTodo(Number.parseInt(id));
	}
	return json({ ok: true });
}

export async function handleDeleteTodo(formData: FormData) {
	const id = formData.get("id");
	if (typeof id === "string") {
		await deleteTodo(Number.parseInt(id));
	}
	return json({ ok: true });
}

app/services/todoService.ts
import type { Todo } from "~/models/todo";

// モックデータをメモリ上で管理
let mockTodos: Todo[] = [
	{ id: 1, title: "Remixを学ぶ", completed: false },
	{ id: 2, title: "TODOアプリを作る", completed: true },
	{ id: 3, title: "コードを理解する", completed: false },
];

export async function getTodos() {
	// APIレスポンスを模倣
	await new Promise((resolve) => setTimeout(resolve, 500));
	return mockTodos;
}

export async function addTodo(title: string) {
	const newTodo: Todo = {
		id: mockTodos.length + 1,
		title,
		completed: false,
	};
	mockTodos = [...mockTodos, newTodo];
	return newTodo;
}

export async function toggleTodo(id: number) {
	mockTodos = mockTodos.map((todo) =>
		todo.id === id ? { ...todo, completed: !todo.completed } : todo,
	);
	return mockTodos.find((todo) => todo.id === id);
}

export async function deleteTodo(id: number) {
	mockTodos = mockTodos.filter((todo) => todo.id !== id);
}

app/components/todo/TodoForm.tsx
import { Form } from "@remix-run/react";

export function TodoForm() {
	return (
		<Form method="post" className="mb-4">
			<div className="flex gap-2">
				<input
					type="text"
					name="title"
					placeholder="新しいTODOを入力"
					className="flex-1 p-2 border rounded"
				/>
				<button
					type="submit"
					name="intent"
					value="add"
					className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
				>
					追加
				</button>
			</div>
		</Form>
	);
}
app/components/todo/TodoItem.tsx
import { Form } from "@remix-run/react";
import type { Todo } from "~/models/todo";

type Props = {
	todo: Todo;
};

export function TodoItem({ todo }: Props) {
	return (
		<li className="flex items-center justify-between p-3 bg-white border rounded shadow-sm">
			<Form method="post" className="flex items-center gap-2">
				<input type="hidden" name="id" value={todo.id} />
				<input type="hidden" name="intent" value="toggle" />
				<input
					type="checkbox"
					checked={todo.completed}
					onChange={(e) => e.currentTarget.form?.submit()}
					className="h-4 w-4"
				/>
				<span className={todo.completed ? "line-through text-gray-500" : ""}>
					{todo.title}
				</span>
			</Form>

			<Form method="post">
				<input type="hidden" name="id" value={todo.id} />
				<button
					type="submit"
					name="intent"
					value="delete"
					className="text-red-500 hover:text-red-700"
				>
					削除
				</button>
			</Form>
		</li>
	);
}

app/components/todo/TodoList.tsx
import type { Todo } from "~/models/todo";
import { TodoItem } from "./TodoItem";

type Props = {
	todos: Todo[];
};

export function TodoList({ todos }: Props) {
	return (
		<ul className="space-y-2">
			{todos.map((todo) => (
				<TodoItem key={todo.id} todo={todo} />
			))}
		</ul>
	);
}

さいごに

Actionも実装すると、だんだんRemixの使い方がわかってきましたね!!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?