第45章:練習:プロフィールカードをServer/Clientで作り分ける🪄💳✨
この章は「プロフィールカード」を作りながら、 Server Component(データ用) と Client Component(操作用) をキレイに分ける練習だよ〜!😆💕
この章のゴール🎯✨
- Server側でプロフィールデータを用意する🧊(安全&速いイメージ)
- Client側で「フォローする」ボタンを動かす🎮(クリック・localStorageなど)
- “見た目は1枚のカード” だけど、中身は Server/Clientの役割分担 になってる状態にする💡
完成イメージ(役割の流れ)🧭✨
1) ルート(ページ)を作る🏠📄
✅ app/profile/page.tsx を作成
import ProfileCardServer from "@/components/profile/ProfileCardServer";
export default function ProfilePage() {
return (
<main style={{ maxWidth: 520, margin: "40px auto", padding: "0 16px" }}>
<h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 16 }}>
プロフィール🌸
</h1>
<ProfileCardServer userId="u_001" />
</main>
);
}
2) Server Component(データ担当)を作る🧊📦
ここは「データを準備して」「Clientに渡す」係だよ〜!🍵✨ (クリック操作やlocalStorageはやらない!)
✅ components/profile/ProfileCardServer.tsx を作成
import ProfileCardClient from "./ProfileCardClient";
type Props = {
userId: string;
};
type Profile = {
id: string;
name: string;
bio: string;
avatarUrl: string;
followerCount: number;
updatedAtISO: string; // ✅ Dateじゃなく文字列で渡す(安全!)
};
async function getProfile(userId: string): Promise<Profile> {
// それっぽく「サーバーで取得してる感」😴💤(なくてもOK)
await new Promise((r) => setTimeout(r, 300));
return {
id: userId,
name: "あかね",
bio: "カフェ巡りと写真が好き☕📸 ゆるっと開発も勉強中〜!",
avatarUrl: "/avatar.png", // public/avatar.png を置く想定🖼️
followerCount: 128,
updatedAtISO: new Date().toISOString(),
};
}
export default async function ProfileCardServer({ userId }: Props) {
const profile = await getProfile(userId);
// ✅ Server → Client へ「シリアライズ可能なデータだけ」渡す
return <ProfileCardClient profile={profile} />;
}
3) Client Component(操作担当)を作る🎮💖
ここが「ボタン押したら変わる!」担当😍 ついでに localStorage で「フォロー中」を記憶しちゃうよ〜!🧠✨
✅ components/profile/ProfileCardClient.tsx を作成
"use client";
import { useEffect, useMemo, useState } from "react";
import styles from "./ProfileCard.module.css";
type Profile = {
id: string;
name: string;
bio: string;
avatarUrl: string;
followerCount: number;
updatedAtISO: string;
};
type Props = {
profile: Profile;
};
export default function ProfileCardClient({ profile }: Props) {
const storageKey = useMemo(() => `followed:${profile.id}`, [profile.id]);
const [followed, setFollowed] = useState(false);
const [count, setCount] = useState(profile.followerCount);
// ✅ ブラウザAPIは Client で!(localStorage)
useEffect(() => {
const saved = localStorage.getItem(storageKey);
if (saved === "1") {
setFollowed(true);
setCount(profile.followerCount + 1);
}
}, [storageKey, profile.followerCount]);
const toggleFollow = () => {
setFollowed((prev) => {
const next = !prev;
localStorage.setItem(storageKey, next ? "1" : "0");
setCount((c) => (next ? c + 1 : c - 1));
return next;
});
};
return (
<section className={styles.card}>
<div className={styles.header}>
<img
src={profile.avatarUrl}
alt={`${profile.name}のアイコン`}
className={styles.avatar}
/>
<div className={styles.headerText}>
<p className={styles.name}>{profile.name}さん🌷</p>
<p className={styles.meta}>フォロワー:{count}人</p>
</div>
</div>
<p className={styles.bio}>{profile.bio}</p>
<button
className={followed ? styles.followedBtn : styles.followBtn}
onClick={toggleFollow}
type="button"
>
{followed ? "フォロー中💖" : "フォローする🤝"}
</button>
<p className={styles.updated}>
更新:{new Date(profile.updatedAtISO).toLocaleString()} ⏰
</p>
</section>
);
}
4) CSS Modulesでカードっぽくする💅✨
✅ components/profile/ProfileCard.module.css を作成
.card {
border: 1px solid #e6e6e6;
border-radius: 16px;
padding: 16px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.06);
background: #fff;
}
.header {
display: flex;
gap: 12px;
align-items: center;
margin-bottom: 12px;
}
.avatar {
width: 64px;
height: 64px;
border-radius: 999px;
object-fit: cover;
border: 1px solid #eee;
}
.headerText {
display: grid;
gap: 4px;
}
.name {
font-size: 18px;
font-weight: 700;
}
.meta {
font-size: 13px;
color: #666;
}
.bio {
margin: 12px 0 14px;
line-height: 1.6;
}
.followBtn,
.followedBtn {
width: 100%;
border: none;
border-radius: 12px;
padding: 12px 14px;
font-weight: 700;
cursor: pointer;
}
.followBtn {
background: #111;
color: #fff;
}
.followedBtn {
background: #ffe3ee;
color: #b1004b;
}
.updated {
margin-top: 10px;
font-size: 12px;
color: #777;
}
5) 画像を置く🖼️✨(超かんたん)
public/avatar.pngを作って、好きな画像をavatar.pngという名前で置いてね📁💕 (WindowsならエクスプローラでpublicにドラッグでOK!🖱️✨)
6) 動作チェック✅🌈
PowerShell でプロジェクトのルートに移動して…
npm run dev
ブラウザで👇にアクセス
http://localhost:3000/profile🏃♀️💨
確認ポイント👀✨
- カードが表示される💳
- ボタン押すと「フォロー中💖」に変わる🎉
- リロードしても状態が残る(localStorage)🧠✨
よくあるハマり🪤(ここだけ見ればだいたい直る😂)
-
ボタンが押せない 😭 → Client側ファイルの先頭に
"use client"がある?🎮 -
localStorage is not defined😵 → ServerでlocalStorage触ってない?(触るのはClientだけ!)🌐 -
Server→Clientに渡したらエラー😵💫 →
DateやMapや関数を渡してない? ✅ この章みたいに 文字列/数値/配列/オブジェクト(JSONっぽい) にしよう📦✨
ミニ課題(5分)⏱️💪
bioをもう1行増やして、改行もきれいに見せてみよう📝✨- 「フォロー中💖」のときだけ、ボタンの文言を
フォロー中(解除する)💔にしてみよう🎭 followerCountを「フォローしたら +1 / 解除したら -1」以外に、 “上限0で止める” ようにしてみよう🧯(マイナス防止!)
これで「Serverはデータ担当🧊 / Clientは操作担当🎮」の分け方が、かなり体に入るはず!😆✨