📝はじめに
記事の内容について
前回作成したマイページに、今回はプロフィール編集フォームを追加していきます!
ログイン中のユーザーが自分の情報を編集し、Supabase の update() で profiles テーブルに反映する処理を実装します。
フォーム部分では React Hook Form と Zod を使って、バリデーションも取り入れていきます!
編集フォームの概要について
ログイン済みユーザーが自身のプロフィール情報(氏名・メールアドレス・電話番号など)を変更できるよう、編集画面を作成します。
Supabase に保存されている profiles テーブルのデータと同期し、保存ボタンを押すと update() で上書きされる仕組みで作成します。
🔧 使用技術・環境
- Next.js (App Router, TypeScript)
- Supabase(Authentication + Database)
- React Hook Form
- Zod(スキーマバリデーション)
📚 本記事の構成(お品書き)
① 🛠 プロフィール編集ページの作成
②🧩 React Hook Form + Zod の導入とバリデーション設定
③💾 Supabase の update() を用いたデータ更新
④🎨 フォームUIの作成と表示確認
⑤🔹 詰まったポイント・補足事項
⑥✅ まとめ & 次回予告
🛠 1. プロフィール編集ページの作成
①/app/mypage/settings/page.tsxを作成する。
こちらのファイルにフォームの定義、supabaseとの通信、バリデーションなどの処理を記載します。
②use clientを冒頭に配置。
💡 補足
use clientとは?
use client ディレクティブをページの冒頭に記述することで、このファイルがクライアント側で動作する React コンポーネントであることをNext.jsに伝えます。
クライアントコンポーネントでは useState や useEffect などのReact Hooksが利用でき、ユーザーの入力やクリックなどの操作にリアルタイムで反応できます。
これを書かないと、ページが「サーバーコンポーネント」として扱われてしまい、フォームの状態管理やSupabaseとの通信に必要なHooksが使えなくなります。
③useEffectを使ってログインユーザーのプロフィール情報を取得するコードを記載。
useEffect(() => {
const fetchProfile = async () => {
const { data: { user } } = await supabase.auth.getUser()
if (!user) return
const { data, error } = await supabase
.from("profiles")
.select("*")
.eq("id", user.id)
.single()
if (!error && data) {
reset({
fullName: data.full_name ?? "",
email: data.email ?? "",
phone: data.phone ?? "",
address: data.address ?? "",
bio: data.bio ?? "",
})
}
}
fetchProfile()
}, [reset])
- fetchProfileという関数の中でasync/awaitを使ってSupabaseからユーザー情報を取得します
- const { data: { user } } = await supabase.auth.getUser()
この行ではログイン中のユーザー情報をSupabaseのauth.getUser()を使って取得しています。さらにuserオブジェクトを取り出すという作業をしています。 - if (!user) return
この行ではユーザーがログインしていない場合、ここで処理を終了します。 - 次にprofilesテーブルからログイン中のユーザープロフィールを取得します。single()を使うことで返り値を1件のオブジェクトとして取得することができます。
- 最後にデータ取得にエラーがなかったらReact Hook Formのreset()関数を使ってフォームに初期値をセットします。
💡 補足
コードを書いていて const { data: { user } } = await supabase.auth.getUser() と const { data, error } = await supabase がそれぞれ何をしているのかが理解しにくかったのでまとめてみます('◇')ゞ
まずconst { data: { user } } = await supabase.auth.getUser()これはSupabaseの認証システム(auth)に登録されているユーザー情報の取得です。例えばidやemailといった認証にかかわる情報が入っています。Supabaseが内部的に管理しているauth.usersテーブルからとってきているということ。
次にconst { data, error } = await supabaseについて。こちらはこの後に
.from("profiles")
.select("*")
.eq("id", user.id)
.single()
という記載があります。これは自分で作成したprofilesテーブルからそのユーザーのプロフィール情報を取得しています。
①auth.getUser()
↓
ログイン中のユーザー情報(auth.usersの中身)を取得。その中に含まれるuser.idを使って
② profilesテーブルから select()
↓
ユーザーのプロフィール情報(氏名・電話番号など)を取得
という流れをしています。
④useFormの初期化とresetの活用
まずuseFormとは何か?
それは「React Hook Form」というライブラリが提供しているReact用のフック関数です!通常のReactではフォームの入力内容やバリデーションを自分でひとつづつuseStateなどで管理する必要がありました...入力欄も多くコードが増えて複雑です。そんな時に活躍するのがuseFormです(≧▽≦)
useFormでできること
- register : 入力欄(imput)とデータを自動で紐づける。UIのところで実践するのでコードで出てくるのはまだ先です。
- handleSubmit : フォームの送信処理(onSubmit)を楽に実装できる
- reset : フォームに初期値をセットしたりリセットができる。
- errors : 入力のバリデーションエラーをまとめて取得できる。
書き方は
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm<FormData>({
resolver: zodResolver(formSchema),
})
こんな感じ。これらの名前はすべてReact Hook Formが決めた関数名なのでそのまま使ってOK!
次にresetについて解説します!
resetって初期化?空っぽにするの?と最初は思っていました(/ω\)
ですがreset()は、入力欄を空にするのではなく、Supabaseから取得したデータを初期状態としてセットするために使います。
書き方はこの部分↓
reset({
fullName: data.full_name ?? "",
email: data.email ?? "",
phone: data.phone ?? "",
address: data.address ?? "",
bio: data.bio ?? "",
})
たとえば「data.full_name ?? ""」これが何をしているかというと、、、data.full_name(profilesテーブルから持ってきたデータのfull_name)がnullまたはundefinedの時は""(空文字)を代わりに使う。データがあればdata.full_nameの値をそのまま使う。という意味になっています。これはSupabaseにまだ登録されていない項目がnulllの場合にクラッシュしないように保険をかける意味があります。
🧩 2. React Hook Form + Zod の導入とバリデーション設定
ここではフォームの入力内容を管理するため、useForm()フックとzodを組み合わせて入力チェック機能を作ります。
①スキーマを定義する
待って、スキーマって何?と思いますよね。お答えします。スキーマとは、、、
実際にコードを書いてみます。まず冒頭の方でZodをimportして。
const formSchema = z.object({
fullName: z.string().min(2, { message: "氏名は2文字以上で入力してください" }),
email: z.string().email({ message: "有効なメールアドレスを入力してください" }),
phone: z.string().optional(),
address: z.string().optional(),
bio: z.string().optional(),
})
このように定義します。たとえばfullNameについて。これはfullNameは文字列で最低2文字は必要です、というルールを定めてます。このルールをまとめたものを「スキーマ」と呼ぶのです。入力のためのルールブックのようなものですね!データベースに保存する前に”これは正しい値?”というのをチェックしてくれます。
それぞれ
- z.string : 値は文字列です
- .min(2, : 2文字未満だったらエラーにしてね
- {message: "" } : エラーが起きたらこう表示してね
ということを表しています。
②zodResolverを使う
ZodのスキーマをReact Hook Formに連携させるにはzodResolverを使います。
import { zodResolver } from "@hookform/resolvers/zod"このようにインポートすることでuseFormの構造を実際に使えるようにします。
③ useFormにスキーマの型を適用する
Zodでバリデーションルール(スキーマ)を定義したあと、その型情報をReact Hook Formでも利用することで、フォームの入力データがどんな形かを一貫して管理できます。
そのために、以下のように useForm
の呼び出し時に z.infer<typeof formSchema>
を使います。
type FormData = z.infer<typeof formSchema>
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm<FormData>({
resolver: zodResolver(formSchema),
})
💡 補足
:「z.infer」とは、Zodで定義したスキーマの型情報をそのまま使える便利な機能です!
これにより、FormData型として「氏名は文字列で必須」「電話番号はオプション」などのルールが自動的に型にも反映され、コーディングミスを防げます。
💾 3. Supabase の update() を用いたデータ更新
では次に「保存」ボタンを押したときの処理、つまり Supabaseのデータベースに対して更新処理を行う処理 を記述していきます!
実際に書いたコードがこちら↓
const onSubmit = async (values: z.infer<typeof formSchema>) => {
const { data: { user } } = await supabase.auth.getUser()
if (!user) return
const { error } = await supabase
.from("profiles")
.update({
full_name: values.fullName,
email: values.email,
phone: values.phone,
address: values.address,
bio: values.bio,
})
.eq("id", user.id)
if (error) {
toast.error("保存に失敗しました")
} else {
toast.success("保存しました")
}
}
ひとつづつ解説していきます。
- values : フォームから渡されるすべての入力されたデータ。
- supabase.auth.getUser() : 再度ログインユーザーを確認。
- .update({ }) : 更新したいカラムを選択します。それぞれvaluesに入っている中で該当のデータを選択しています!
- .eq("id",user.id) : 対象のレコードをログイン中のユーザーに限定しています。
- toast : 通知です。成功・失敗時にそれぞれ通知でフィードバックします。
💡補足:
toast通知を表示するには?
本記事では通知表示に sonner というライブラリを使っています。
toast.success() や toast.error() を簡単に使える便利なツールです。
詳しくは公式ドキュメント:https://sonner.emilkowal.dev/ を参考にしてみてください。
まだsonner
を導入していない場合は以下でインストールできますpnpm add sonner
app/layout.tsx などのレイアウトコンポーネントに を配置しておきましょう。
🎨 4. フォームUIの作成と表示確認
①実際にUIを構築して表示してみます!
で、実際に書いたコードがこちら↓
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4 p-4 max-w-xl mx-auto">
<div>
<label>氏名</label>
<input {...register("fullName")} className="input" />
<p className="text-red-500">{errors.fullName?.message}</p>
</div>
<div>
<label>メールアドレス</label>
<input {...register("email")} className="input" />
<p className="text-red-500">{errors.email?.message}</p>
</div>
{/* 以下略 */}
<button type="submit" className="btn btn-primary">保存する</button>
</form>
💡 補足
ここでregisterが出てきます。register("fullName") のように記述することで、フォームの入力欄とReact Hook Formが自動で紐づきます。
②動作確認をしてみます!
と思ったけれど、マイページからぷフィール編集ページに遷移する仕組みがない!!!
えっと、マイページに遷移リンクをつけます(; ・`д・´)
前回作成した/mypage/page.tsxに以下コードを追加しました。
import Link from "next/link"
<Link href="/mypage/settings">
<button className="btn btn-outline">プロフィール設定へ</button>
</Link>
結果は、、、
「プロフィール設定へ」のボタンを出すことができました!(前回と登場人物が変わっていますが気にせず次行きましょう!)
設定ページに飛んでみます。
できました!ちゃんとsupabaseから情報を取得して表示できています。ここまではクリア。では実際に変更してちゃんと保存されるかやってみます。名前と、メールアドレス、電話番号を変えて保存してみます。
結果はできない!コンソールには404エラー。調べたら更新リクエストが失敗しているらしい。原因は恐らく電話番号(ついでに住所と自己紹介。)profilesテーブルにこのカラムが存在していないから、変更しようとしてもエラーになるのでは???
なら追加しちゃおう!ということでprofilesテーブルにphone,address,bioをそれぞれtext型で追加しました。
もう一度保存して、リロード。
できた!ちゃんと更新されました!supabaseの方もちゃんと更新されているのを確認できました。
完成\(^o^)/
🔹5. 詰まったポイントと学び
① 動かない原因はカラム不足だった!
フォームの保存時に**「400 Bad Request」**が出てしまったのですが、これはSupabaseのprofilesテーブルに存在しないカラム(phoneやaddressなど)を更新しようとしたために起きていました。
Supabaseでは、存在しないカラム名でupdate()やinsert()を行うと、エラーが発生します。
これを解決するために、SQLエディタで以下のようにカラムを追加しました。
ALTER TABLE profiles ADD COLUMN phone text;
ALTER TABLE profiles ADD COLUMN address text;
ALTER TABLE profiles ADD COLUMN bio text;
この修正後、再度保存を行うと無事に更新できるようになりました!
②そもそもページ遷移の構造を作っていなかった!
これは準備不足💦。認識が甘かったですね。すぐに気づいて実装までできたので大きな進歩です!
③ 編集フォームの構造とリセット処理の理解が大事だった
React Hook Formのreset()やZodのz.inferなどは最初は難しく感じましたが、実際に動かしてみて次第に理解できました。でもやっぱり難しい💦。もっと理解を深めていきたいと感じたところでした。
特に
reset({
fullName: data.full_name ?? "",
...
})
のように「nullの場合でも安全に扱う工夫」が、バグを防ぐために大切だと学びました。
✅6. 次回に向けて
今回はプロフィール編集まで実装できましたが、まだセキュリティ的に不十分な状態です。
このままだと、誰でも他のユーザーのプロフィール情報にアクセスできてしまう可能性があります…。
それを防ぐために登場するのが Supabase の RLS(Row Level Security)!
ということで、
📌 予告
次回はこの「RLS」について、
- なぜ必要?
- どんな設定が必要?
- 実際にどんなポリシーを書くの?
といった基本からしっかり学んでいきます!
お楽しみに!
最期までお付き合いいただき、ありがとうございました!