Client дээр өгөгдөл татах
Ихэнх өгөгдлийг Server Component-д татах нь хамгийн зөв арга. Гэхдээ зарим тохиолдолд хөтөч дээр өгөгдөл татах шаардлага гардаг — хэрэглэгч товч дарсны дараа шинэчлэгдэх мэдээлэл, бодит цагийн өгөгдөл, хэрэглэгчийн үйлдлээс хамаарсан дата зэрэг. Энэ хичээлд client-side data fetching-г судална.
Хэзээ client дээр татах вэ?
Дараах тохиолдолд client-side fetch ашиглана:
- Хэрэглэгч товч дарсны дараа өгөгдөл хэрэгтэй болох
- Бодит цагийн өгөгдөл (шинэ мэдэгдэл, чат г.м.)
- Хэрэглэгчийн харилцан үйлчлэл-ээс хамаарсан дата (хайлт, шүүлтүүр)
- Зөвхөн нэвтэрсний дараа харагдах өгөгдөл
Эдгээр биш бол Server Component + async/await ашиглах нь илүү зөв.
useState + useEffect — Сонгодог арга
// components/lesson/LessonProgress.tsx
"use client";
import { useState, useEffect } from "react";
interface Progress {
courseSlug: string;
lessonSlug: string;
completedAt: string;
}
interface Props {
userId: string;
}
export default function LessonProgress({ userId }: Props) {
const [progress, setProgress] = useState<Progress[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function fetchProgress() {
try {
const res = await fetch(`/api/progress?userId=${userId}`);
if (!res.ok) {
throw new Error("Явц татахад алдаа гарлаа");
}
const data: Progress[] = await res.json();
setProgress(data);
} catch (err) {
setError(err instanceof Error ? err.message : "Алдаа гарлаа");
} finally {
setLoading(false);
}
}
fetchProgress();
}, [userId]);
if (loading) {
return (
<div className="space-y-2">
{[1, 2, 3].map((i) => (
<div key={i} className="h-8 bg-[#1e293b] rounded animate-pulse" />
))}
</div>
);
}
if (error) {
return <p className="text-red-400 text-sm">{error}</p>;
}
if (progress.length === 0) {
return (
<p className="text-slate-400 text-sm">Дуусгасан хичээл байхгүй байна.</p>
);
}
return (
<ul className="space-y-1">
{progress.map((p) => (
<li
key={`${p.courseSlug}-${p.lessonSlug}`}
className="text-slate-400 text-sm flex items-center gap-2"
>
<span className="text-green-400">✓</span>
{p.courseSlug} / {p.lessonSlug}
</li>
))}
</ul>
);
}
Товч дарсны дараа fetch — Mutation
Хэрэглэгч хичээл дуусгах товч дарахад API руу өгөгдөл илгээж, state шинэчлэх загвар:
// components/lesson/CompleteButton.tsx
"use client";
import { useState } from "react";
interface Props {
courseSlug: string;
lessonSlug: string;
initialCompleted: boolean;
}
export default function CompleteButton({
courseSlug,
lessonSlug,
initialCompleted,
}: Props) {
const [completed, setCompleted] = useState(initialCompleted);
const [saving, setSaving] = useState(false);
async function handleComplete() {
if (completed || saving) return;
setSaving(true);
try {
const res = await fetch("/api/progress", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ courseSlug, lessonSlug, passed: true }),
});
if (!res.ok) {
throw new Error("Хадгалахад алдаа гарлаа");
}
setCompleted(true);
} catch (err) {
console.error(err);
alert("Хичээл хадгалахад алдаа гарлаа. Дахин оролдоно уу.");
} finally {
setSaving(false);
}
}
return (
<button
onClick={handleComplete}
disabled={completed || saving}
className={`px-6 py-3 rounded-lg font-medium transition-colors ${
completed
? "bg-green-900 text-green-400 cursor-default"
: saving
? "bg-[#1e293b] text-slate-400 cursor-wait"
: "bg-indigo-600 text-white hover:bg-indigo-500"
}`}
>
{completed
? "Хичээл дууссан — +10 XP"
: saving
? "Хадгалж байна..."
: "Дуусгах"}
</button>
);
}
Хайлтын автоматаар шинэчлэгдэх UI
Хэрэглэгч хайлтын талбарт бичих бүрд өгөгдөл автоматаар шинэчлэгдэх жишээ:
// components/CourseSearch.tsx
"use client";
import { useState, useEffect } from "react";
import Link from "next/link";
interface Course {
slug: string;
title: string;
}
export default function CourseSearch({ allCourses }: { allCourses: Course[] }) {
const [query, setQuery] = useState("");
const [results, setResults] = useState<Course[]>(allCourses);
useEffect(() => {
if (!query.trim()) {
setResults(allCourses);
return;
}
const filtered = allCourses.filter((c) =>
c.title.toLowerCase().includes(query.toLowerCase()),
);
setResults(filtered);
}, [query, allCourses]);
return (
<div>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Курс хайх..."
className="w-full bg-[#0f172a] border border-[#1e293b] rounded-lg px-4 py-2 text-white placeholder:text-slate-500 focus:outline-none focus:border-indigo-500"
/>
<p className="text-slate-400 text-sm mt-2">
{results.length} курс олдлоо
</p>
<ul className="mt-3 space-y-1">
{results.map((c) => (
<li key={c.slug}>
<Link
href={`/courses/${c.slug}`}
className="text-indigo-400 hover:underline text-sm"
>
{c.title}
</Link>
</li>
))}
</ul>
</div>
);
}
Энэ жишээнд Server Component нь allCourses өгөгдлийг нэг удаа татаж prop-оор дамжуулна. Client Component нь зөвхөн шүүх ажиллагааг хийнэ — давтан fetch хийхгүй.
Server vs Client татах — Хэзээ аль нь?
| Тохиолдол | Хаана татах |
| ---------------------------------- | --------------------------------------- |
| Хуудас нээгдэхэд өгөгдөл хэрэгтэй | Server Component |
| Хэрэглэгч нэвтрэхийн өмнө харагдах | Server Component |
| Товч дарсны дараа өгөгдөл хэрэгтэй | Client Component + fetch |
| Бодит цагийн шинэчлэлт | Client Component + setInterval |
| Хайлт, шүүлтүүр | Client Component (эсвэл searchParams) |
Дараагийн хичээлд:
Next.js-н кэш систем судална. fetch кэшлэх, revalidate тохируулах, кэш хэрхэн ажилладгийг ойлгоод хуудасны гүйцэтгэлийг дээд зэргээр сайжруулна.