はじめに
前回の続きとなります。
今回はバックエンド側の実装をし、フロントとの繋ぎ込みを実施します!
シリーズもの
-
Step 1: 基本的なセットアップとTailwindCSS導入
Remixプロジェクトの作成、TailwindCSSのセットアップ、静的なTODOリストの表示 -
Step 2: バックエンドとの疎通
Remixのloader関数を使ったデータフェッチ、型定義の作成、ローディング状態の実装 -
Step 3: アクションの実装
TODOの追加・更新・削除機能の実装、コンポーネントの分割とリファクタリング -
Step 4: データベース連携
PrismaとSQLiteを使用したデータの永続化、マイグレーションの実行、CRUD操作の実装
成果物
なんのて変哲もない表示です。ただ、前回まではフロントの固定値を返却していましたが、今回はloaderを使ってバックエンドの値を元にバインドしています。
Remixのloader関数とは?
サーバーサイドでデータを準備し、コンポーネントに届けるRemixの特別な関数
loaderの3つのポイント
- サーバーサイドで実行される
- ページアクセス時に自動的に呼ばれる
- 準備したデータはuseLoaderDataで簡単に取得できる
シンプルな例
// サーバーサイド: データを準備
export async function loader() {
const todos = ["Remixを学ぶ", "TODOアプリを作る"];
return json({ todos });
}
// クライアントサイド: データを使用
export default function Page() {
const { todos } = useLoaderData<typeof loader>();
return <ul>{todos.map(todo => <li>{todo}</li>)}</ul>;
}
なぜloaderが便利か?
- データ取得のロジックをサーバーに集中できる
- コンポーネントはデータの表示に集中できる
- 型安全にデータをやり取りできる
今回やりたいこと
ソースコード
ディレクトリ構成
~/develop/remix_sample (feat/step2_api)$ tree app/
app/
├── entry.client.tsx
├── entry.server.tsx
├── models
│ └── todo.ts # バックエンドとフロントの共通コンポーネント
├── root.tsx
├── routes
│ └── _index.tsx # フロント
├── services
│ └── todoService.ts # バックエンド
└── tailwind.css
4 directories, 7 files
app/services/todoService.ts
import type { Todo } from "~/models/todo";
// モックデータ
const 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;
}
app/models/todo.ts
export type Todo = {
id: number;
title: string;
completed: boolean;
};
app/routes/_index.tsx
import { json } from "@remix-run/node";
import { useLoaderData, useNavigation } from "@remix-run/react";
import type { MetaFunction } from "@remix-run/node";
import { getTodos } from "~/services/todoService";
export const meta: MetaFunction = () => {
return [
{ title: "TODO App" },
{ name: "description", content: "Simple TODO App with Remix" },
];
};
// loader(サーバーサイド)
export async function loader() {
const todos = await getTodos();
return json({ todos });
}
// コンポーネント(クライアントサイド)
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>
) : (
<>
{/* TODO追加フォーム */}
<div className="mb-4">
<input
type="text"
placeholder="新しいTODOを入力"
className="w-full p-2 border rounded"
/>
</div>
{/* TODOリスト */}
<ul className="space-y-2">
{todos.map((todo) => (
<li
key={todo.id}
className="flex items-center justify-between p-3 bg-white border rounded shadow-sm"
>
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={todo.completed}
onChange={() => {}} // 後で実装
className="h-4 w-4"
/>
<span
className={
todo.completed ? "line-through text-gray-500" : ""
}
>
{todo.title}
</span>
</div>
<button
type="button"
className="text-red-500 hover:text-red-700"
>
削除
</button>
</li>
))}
</ul>
</>
)}
</div>
);
}
さいごに
バックエンドを実装したことで、Remixの理解がちょっとだけ深まりました!