第195章:ユーザープロフィールの取得と表示
今日は「ログイン中のユーザーのプロフィール」をDBから取ってきて、画面に表示します😊 (例:表示名・自己紹介・アバターなど)🪄
ここでは Supabase を例にします(Firebaseでも考え方はほぼ同じだよ!)🔥
この章のゴール 🎯
- ✅ ログイン中ユーザーの
idを取れる - ✅
profilesテーブルから自分のプロフィールを取得できる - ✅ ローディング中/エラー時も“それっぽく”表示できる
全体の流れ(図解)🗺️
1) Supabase側:profilesテーブルを用意する 🐘🛠️
Supabaseでは、auth.users は直接APIで触れないので、publicスキーマに profiles を作るのが定番です。(Supabase)
さらに RLS(行レベルセキュリティ) をONにして「本人だけ見れる」ようにします🔐(Supabase)
SupabaseのSQL Editorで、まずこれ👇(コピペOK)
create table if not exists public.profiles (
id uuid primary key references auth.users(id) on delete cascade,
username text,
bio text,
avatar_url text,
updated_at timestamptz default now()
);
alter table public.profiles enable row level security;
-- 読み取り:自分のプロフィールだけOK
create policy "Profiles are readable by owner"
on public.profiles
for select
using (auth.uid() = id);
-- 更新:自分のプロフィールだけOK
create policy "Profiles are updatable by owner"
on public.profiles
for update
using (auth.uid() = id);
🔥ポイント:
idを auth.users のidと一致させるのがラク!
2) フロント:Supabaseクライアントを作る 🔑
インストール(Windows / PowerShellでもOK)💻
npm i @supabase/supabase-js
.env.local を作る(Vite)🧪
Viteは VITE_ で始まる環境変数だけ フロントで読めます。(vitejs)
VITE_SUPABASE_URL=https://xxxxxxxx.supabase.co
VITE_SUPABASE_ANON_KEY=xxxxxxxxxxxxxxxxxxxxxxxx
src/lib/supabase.ts
import { createClient } from "@supabase/supabase-js";
const url = import.meta.env.VITE_SUPABASE_URL as string | undefined;
const anonKey = import.meta.env.VITE_SUPABASE_ANON_KEY as string | undefined;
if (!url || !anonKey) {
throw new Error("Supabaseの環境変数が見つからないよ🥺 .env.local を確認してね!");
}
export const supabase = createClient(url, anonKey);
3) ログイン中ユーザーを取る 👤
Supabaseで「今のユーザー」を安全に取るなら getUser() が基本です。(Supabase)
(※フロントでもOK!)
src/features/auth/useCurrentUser.ts
import { supabase } from "../../lib/supabase";
export async function getCurrentUser() {
const { data, error } = await supabase.auth.getUser();
if (error) throw error;
return data.user; // ログインしてないなら null
}
4) プロフィールをDBから取る 📄✨
型を作る(TypeScript)🧩
src/types/profile.ts
export type Profile = {
id: string;
username: string | null;
bio: string | null;
avatar_url: string | null;
updated_at: string | null;
};
取得関数を作る 🧲
src/features/profile/getProfile.ts
import { supabase } from "../../lib/supabase";
import type { Profile } from "../../types/profile";
export async function getProfile(userId: string): Promise<Profile> {
const { data, error } = await supabase
.from("profiles")
.select("id, username, bio, avatar_url, updated_at")
.eq("id", userId)
.single();
if (error) throw error;
return data as Profile;
}
5) React v19らしく:use + Suspense で表示する 🌈😺
Reactの use(Promise) は、Promiseの結果を レンダー中に読めて、Suspense の fallback が効きます。(React)
ただし、毎回new Promiseだとつらいので、Promiseをキャッシュします(ここ大事!)💡
Promiseキャッシュ(超シンプル版)🧊
src/features/profile/profileResource.ts
import type { Profile } from "../../types/profile";
import { getProfile } from "./getProfile";
const cache = new Map<string, Promise<Profile>>();
export function getProfilePromise(userId: string) {
const hit = cache.get(userId);
if (hit) return hit;
const p = getProfile(userId);
cache.set(userId, p);
return p;
}
// ログアウト時などに呼ぶ用(任意)
export function clearProfileCache() {
cache.clear();
}
エラーバウンダリ(最小)🧯
src/components/ErrorBoundary.tsx
import React from "react";
type Props = {
fallback?: React.ReactNode;
children: React.ReactNode;
};
type State = {
hasError: boolean;
};
export class ErrorBoundary extends React.Component<Props, State> {
state: State = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return this.props.fallback ?? <p>エラーだよ〜🥺</p>;
}
return this.props.children;
}
}
プロフィール画面 💖
src/pages/ProfilePage.tsx
import { Suspense, use } from "react";
import { getCurrentUser } from "../features/auth/useCurrentUser";
import { getProfilePromise } from "../features/profile/profileResource";
import { ErrorBoundary } from "../components/ErrorBoundary";
function ProfileContent() {
const user = use(getCurrentUser()); // user or null
if (!user) return <p>ログインしてないみたい…😢(ログイン画面へ)</p>;
const profile = use(getProfilePromise(user.id));
return (
<div style={{ maxWidth: 520, margin: "24px auto", padding: 16 }}>
<h1>プロフィール 👤✨</h1>
<div style={{ display: "flex", gap: 12, alignItems: "center", marginTop: 12 }}>
<img
src={profile.avatar_url ?? "https://placehold.co/80x80"}
width={80}
height={80}
style={{ borderRadius: "50%" }}
alt="avatar"
/>
<div>
<div style={{ fontSize: 18, fontWeight: "bold" }}>
{profile.username ?? "(名前未設定)"}
</div>
<div style={{ opacity: 0.8 }}>{user.email}</div>
</div>
</div>
<div style={{ marginTop: 16 }}>
<h2>ひとこと 📝</h2>
<p style={{ whiteSpace: "pre-wrap" }}>
{profile.bio ?? "(自己紹介がまだないよ〜🙂)"}
</p>
</div>
</div>
);
}
export default function ProfilePage() {
return (
<ErrorBoundary fallback={<p>プロフィールの読み込みで失敗したよ🥺(RLSとかSQL見てみて!)</p>}>
<Suspense fallback={<p>プロフィール読み込み中…⏳✨</p>}>
<ProfileContent />
</Suspense>
</ErrorBoundary>
);
}
取得のタイミング(シーケンス図)⏱️
動作チェック ✅🎉
-
npm run devで起動 -
ログインした状態でプロフィール画面へ
-
profilesに自分の行が無い場合は、まだ表示できないので- Supabase側で
profilesに行を作る - または「サインアップ時に自動作成」(次の章以降でやると気持ちいい😙)
- Supabase側で
よくあるつまづき 🥺🧷
profilesが取れない(403/401) → RLSポリシーが無い/間違いのことが多いよ🔐(Supabase)import.meta.env...がundefined→.env.localのキーがVITE_で始まってるか確認!(vitejs)use(Promise)で無限に読み込み → Promiseを毎回作ってるかも!今回みたいにキャッシュしよ🧊(React)
ミニ練習(やってみよ〜🧠💪)
- ✅
profilesにfavorite_color(好きな色)カラムを追加して表示 🎨 - ✅
updated_atを「最終更新:YYYY/MM/DD」みたいに整形して表示 📅 - ✅ 「プロフィール未作成なら作成画面へ」って分岐を入れてみる 🚪✨
次の章(第196章)で「ログイン/新規登録フォーム」を作るなら、サインアップ直後にprofilesを自動作成する流れにするとめっちゃ気持ちいいよ😆💖