Next.js / Client дээр өгөгдөл татах

Client дээр өгөгдөл татах

Ихэнх өгөгдлийг Server Component-д татах нь хамгийн зөв арга. Гэхдээ зарим тохиолдолд хөтөч дээр өгөгдөл татах шаардлага гардаг — хэрэглэгч товч дарсны дараа шинэчлэгдэх мэдээлэл, бодит цагийн өгөгдөл, хэрэглэгчийн үйлдлээс хамаарсан дата зэрэг. Энэ хичээлд client-side data fetching-г судална.

Хэзээ client дээр татах вэ?

Дараах тохиолдолд client-side fetch ашиглана:

  • Хэрэглэгч товч дарсны дараа өгөгдөл хэрэгтэй болох
  • Бодит цагийн өгөгдөл (шинэ мэдэгдэл, чат г.м.)
  • Хэрэглэгчийн харилцан үйлчлэл-ээс хамаарсан дата (хайлт, шүүлтүүр)
  • Зөвхөн нэвтэрсний дараа харагдах өгөгдөл

Эдгээр биш бол Server Component + async/await ашиглах нь илүү зөв.

useState + useEffect — Сонгодог арга

tsx
// 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 шинэчлэх загвар:

tsx
// 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

Хэрэглэгч хайлтын талбарт бичих бүрд өгөгдөл автоматаар шинэчлэгдэх жишээ:

tsx
// 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 тохируулах, кэш хэрхэн ажилладгийг ойлгоод хуудасны гүйцэтгэлийг дээд зэргээр сайжруулна.