はじめに
今回は、前回に続いて、NextjsとSupabaseを触りながら基本機能を確認していきたいと思います。
以下の動画を参考に進めていきます。
今回は特にsupabaseのデータベースの機能としてテーブル作成とそれに伴う入力フォーム追加を確認していきます。
前回までの振り返り
前回までで、以下の内容を進めてきました。
- Nextjsとsupabaseの環境構築
- プロジェクトの作成
- supabeseの認証情報をプロジェクトに反映
- ページ作成
- トップページ
- Aboutページ(何らかのコンテンツページ)
- ログイン
- supabaseの匿名ログイン設定
- Nextjsのログイン、ログアウト機能追加
その結果、ディレクトリ構成は以下のようになっています。
※.env.local
など隠しファイルは表示されておりません
.
|-- README.md
|-- actions
| `-- auth.ts
|-- app
| |-- about
| | `-- page.tsx
| |-- data
| | `-- auth.ts
| |-- favicon.ico
| |-- globals.css
| |-- layout.tsx
| |-- login
| | `-- page.tsx
| |-- mypage
| | `-- page.tsx
| `-- page.tsx
|-- bun.lockb
|-- components
| |-- footer.tsx
| |-- header.tsx
| `-- ui
| `-- button.tsx
|-- components.json
|-- lib
| |-- supabase
| | |-- client.ts
| | `-- server.ts
| `-- utils.ts
|-- next-env.d.ts
|-- next.config.mjs
|-- node_modules
| |-- ・・・
| |-- 〜省略〜
| `-- ・・・
|-- package.json
|-- postcss.config.mjs
|-- public
| |-- next.svg
| `-- vercel.svg
|-- supabase
| |-- config.toml
| |-- functions
| `-- seed.sql
|-- tailwind.config.ts
`-- tsconfig.json
主な更新箇所としては、以下の通りです。
- actionsディレクトリにauth.tsを作成して、サインインとサインアウト時に呼び出すsupabaseの処理を追加しています。
- appディレクトリに、ディレクトリとpage.tsxを追加して前述の追加ページを作成しています。
- componentsディレクトリには、header.tsxとfooter.tsxを追加しています。layout.tsxの記述を整理するため、headerとfooterを切り出して記述しています。
supabaseのデータベース機能
supabaseはPostgreSQLを搭載したBackend as a Serviceで様々な機能を有しているようですが、私自信、データベース自体の知識に明るく無いため、基本的なところを確認がてら整理していきます。
データベースの基本構造
基本ではございますが、データベースの構造は以下のようになっており、SupabaseではこれらをGUIで確認、操作できるよう画面が用意されております。
- Database:作成したSupabaseプロジェクト
- schema:(例)Auth
- table:(例)users
- aaaさん
- bbbさん
- cccさん
- table:(例)users
- schema:(例)Auth
TableEditor>schema:auth>usersを選択してみると以下のように匿名ログインしたユーザーが確認できます。
schemaの中の各テーブルについて、鍵マークが付いているものがあります。こちらは、ユーザー向けには公開しないようなテーブルを指しています。
publicテーブルの作成
試しにpublicで使用するテーブルを作成してみます。
NewTableを選択します。
以下の内容で、商品を想定したテーブルを作成します。
ポイントとしては以下の点です。入力できたらsaveを選択して作成します。
- PostgreSQLの慣習であるsnake_case(アンダースコアで単語を繋ぐ)は、camelCase(頭文字を大文字にして単語を繋ぐ)で書き直す
- Supabaseとしては今後キャメルケースによせていく話も出ているため
- isNullableのチェックを外しておく
- 空入力を許す場合はチェックしておく。今回は商品のため、金額や商品名は必ず必要な情報のため全てチェックは外しておきます。
テーブルの操作権限を設定
Authentication>Policies>create policyを選択します。
まずは以下の通り、ログインしているユーザーのみテーブルへのアイテム追加をできるようにします。
右のテンプレートのINSERTを選択して利用します。Policy Nameを入力できたらsave policyを選択します。
続いて、読み取りについては全てのユーザーが実施できるようにします。
SELECTテンプレートを使用して、以下の通り設定して保存します。
商品追加フォームを作成
ReactHookFormを使用して商品追加フォームを作成していきます。
まずCmd+Shift+P
でコマンドパレットを立ち上げて、shadcn/ui Add New component
を選択しform
を検索して実行します。するとインポートが開始します。
以下のコマンドを実行することでもインポート可能です。
% bunx shadcn-ui add form
✔ Done.
ここで併せてインポートするための雛形を公式から取得します。
スキーマの作成
Usage章から以下の部分をコピーして、mypage/page.tsxに追加します。
import { z } from "zod"
const formSchema = z.object({
username: z.string().min(2).max(50),
})
ポイントは以下の通りです。
- coerceを追加する
- htmlのフォームの値は基本的に文字列で管理される。しかし、今回の商品の金額のような場合は、数字として渡したいのでこのようなケースで使用する。
今回の場合、以下のような記述になります。
const formSchema = z.object({
amount: z.coerce.number().min(1),
name: z.string().min(1).max(255),
})
formの定義
スキーマの作成と同様に公式から雛形をコピーして、既存のコードに該当箇所にペーストして組み込みます。
// 1. Define your form.
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
username: "",
},
})
type定義を切り出して以下のような記述にします。
type FormType = z.infer<typeof formSchema>;
export default async function page() {
const user = await currentUser();
const form = useForm<FormType>({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
amount: 0,
},
})
例によって、以下のようにimport不足で問題が表示される際は、該当箇所の最後の文字を消して再入力すると、import追加のサジェストが表示されるので、選択してimportの追加をします。
以下の例では、一番上のreact-hook-formのuseFormをインポートします。
formの構築
こちらも同様に公式からのコードを雛形として流用します。
以下の2箇所を適切な行に挿入します。
import { Button } from "@/components/ui/button"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="shadcn" {...field} />
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
)
フォームの記述は以下の通りに更新します。
const onSubmit = () => {
}
return (
<div className="p-6">
<h1>マイページ</h1>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit, () => {
alert('error')
})} className="space-y-8">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="商品1" {...field} />
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="amount"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="$1000" {...field} />
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
</div>
)
}
Inputを追加しているので、shadcn/ui Add New component
を使用して、以下の通りインポートしておきます。
% bunx shadcn-ui add input
✔ Done.
ポイントは以下の通りです。
- 以下の箇所で入力を受取りとエラー処理をしています。
- フォームの入力を受取り、formSchemaの型に合わなければerrorを返す様になっています。
<form onSubmit={form.handleSubmit(onSubmit, () => { alert('error') })} className="space-y-8">
- placeholderでは、入力を受け取る際の例文を入れておくことができます。
/mypage/page.tsxは以下の記述になっています。
import { redirect } from "next/navigation";
import { currentUser } from "../data/auth"
import { Button } from "@/components/ui/button"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { z } from "zod"
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
const formSchema = z.object({
amount: z.coerce.number().min(1),
name: z.string().min(1).max(255),
})
type FormType = z.infer<typeof formSchema>;
export default async function page() {
const user = await currentUser();
const form = useForm<FormType>({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
amount: 0,
},
})
if (!user) {
redirect('/login');
}
const onSubmit = () => {
}
return (
<div className="p-6">
<h1>マイページ</h1>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit, () => {
alert('error')
})} className="space-y-8">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="商品1" {...field} />
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="amount"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="$1000" {...field} />
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
</div>
)
}
問題の修正
上記までで商品の入力フォームを作成しましたが、このままではエラーとなります。
以下が原因になっています。
- /mypage/page.tsxは、useFormを追加してReact-Hookを使用するように変更してしました。この変更により、
"use client"
をページの最上部に挿入して、クライアントコンポーネントとして記述しなければならなくなりました。 - しかし、
"use client"
をそのまま挿入してしまうとasync
関数が使用できなくなります。そうすると付随してawait currentUser
を用いた、ログインユーザーの状態を後半の記述で取り出すというアプローチができなくなります。
この動画のnino+さんは、この問題をページ専用のcomponents階層を追加して、フォームを切り出すことで解決しています。こちらは次回以降で試していこうと思います。
まとめ
今回は、Supabaseの基本機能としてテーブル作成とそれに伴う入力フォーム追加を確認していきました。次回で、エラー解決から勧めていきたいと思います。
最後までご覧いただきありがとうございました。
PostgreSQL icon by Icons8
コメント