😉
Remix + Clerk + SupabaseでTodoアプリを作る
app\routes\page.tsx
import { json, LoaderFunction, ActionFunction,LoaderFunctionArgs } from '@remix-run/node';
import { useLoaderData, Form, redirect } from '@remix-run/react';
import { useEffect, useRef } from 'react';
import { getAuth } from "@clerk/remix/ssr.server";
import { createClient } from '@supabase/supabase-js';
// todo の型を定義
interface Todo {
id: number;
title: string;
}
// Supabase クライアントを生成するユーティリティ関数
const getSupabaseClient = async (args: LoaderFunctionArgs) => {
const { getToken } = await getAuth(args);
const token = await getToken({ template: 'supabase' });
if (!token) {
throw new Response("Token is missing", { status: 401 });
}
return createClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_KEY!, {
global: { headers: { Authorization: `Bearer ${token}` } },
});
};
// Todo アイテムを取得するためのローダー関数
export const loader: LoaderFunction = async (args) => {
const supabase = await getSupabaseClient(args);
const { data, error } = await supabase.from('todos').select('*');
if (error) throw new Response("Failed to fetch todos", { status: 500 });
return json(data);
};
// CRUD 操作を処理するアクション関数
export const action: ActionFunction = async (args) => {
const supabase = await getSupabaseClient(args);
const formData = await args.request.formData();
const method = formData.get("_method") as string;
const title = formData.get("title");
const { userId } = await getAuth(args);
switch (method) {
case "create":
if (typeof title === "string" && title.length > 0) {
await supabase.from('todos').insert({ title, user_id: userId });
return redirect("/page");
}
break;
case "update":
await supabase.from('todos').update({ title: formData.get("title") as string }).match({ id: parseInt(formData.get("id") as string) });
break;
case "delete":
await supabase.from('todos').delete().match({ id: parseInt(formData.get("id") as string) });
break;
}
return redirect("/page");
};
// Todo アイテムを表示するためのコンポーネント
function TodoRow({ todo }: { todo: Todo }) {
return (
<tr>
<td>{todo.title}</td>
<td>
<Form method="post">
<input type="hidden" name="_method" value="update" />
<input type="hidden" name="id" value={todo.id.toString()} />
<input type="text" name="title" defaultValue={todo.title} />
<button type="submit">更新</button>
</Form>
</td>
<td>
<Form method="post">
<input type="hidden" name="_method" value="delete" />
<input type="hidden" name="id" value={todo.id.toString()} />
<button type="submit">削除</button>
</Form>
</td>
</tr>
);
}
export default function Page() {
const todos = useLoaderData<Todo[]>();
const formRef = useRef<HTMLFormElement>(null);
useEffect(() => {
if (formRef.current) {
formRef.current.reset();
}
}, [todos]); // Reset form when todos change, indicating a form submission happened
return (
<div className="container mx-auto px-4 py-5">
<h1 className="text-2xl font-bold text-gray-900 mb-4">Todo List</h1>
<Form ref={formRef} method="post" className="mb-6 flex">
<input type="hidden" name="_method" value="create" />
<input
type="text"
name="title"
placeholder="新しいTodo"
className="border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 rounded-md shadow-sm mr-4 flex-1"
/>
<button
type="submit"
className="px-4 py-2 bg-blue-500 hover:bg-blue-700 text-white font-bold rounded-lg"
>
Todoを追加
</button>
</Form>
<table className="min-w-full leading-normal">
<thead>
<tr>
<th className="px-5 py-3 border-b-2 border-gray-200 bg-gray-100 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
タイトル
</th>
<th className="px-5 py-3 border-b-2 border-gray-200 bg-gray-100 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
操作
</th>
</tr>
</thead>
<tbody>
{todos.map((todo) => (
<TodoRow key={todo.id} todo={todo} />
))}
</tbody>
</table>
</div>
);
}
気になるところメモった
ローダーのargsの型
import {LoaderFunctionArgs } from '@remix-run/node';
const getSupabaseClient = async (args: LoaderFunctionArgs) =>{}
//~~~~~~~~~~~~
export const loader: LoaderFunction = async (args) => {
const supabase = await getSupabaseClient(args);
todoの型定義
// todo の型を定義
interface Todo {
id: number;
title: string;
}
テーブルの1行のコンポーネントの引数に型をセット
// Todo アイテムを表示するためのコンポーネント
function TodoRow({ todo }: { todo: Todo }) {return}
useLoaderDataの呼び出しにもジェネリックで配列表記[]をつけて記載する
export default function Page() {
const todos = useLoaderData<Todo[]>();
<Form>にはref={formRef}をつける
依存するステートはuseLoaderDataのステートである[todos]です。
ボタン押すとアクションでSupabaseに値が送信されて、ローダーで読み取りして再度レンダリング、そしてuseEffect
でformRef
が初期化されます。
export default function Page() {
const todos = useLoaderData<Todo[]>();
const formRef = useRef<HTMLFormElement>(null);
useEffect(() => {
if (formRef.current) {
formRef.current.reset();
}
}, [todos]);
再度、TODOアプリを作り直しました。
Discussion