MATIN MOLKARA
SERIES: آموزش Next js — PART 12 OF 28

قسمت دوازدهم - loading، error و not-found در Next.js

بعد از اینکه داده واقعی را وارد صفحه‌ها می‌شود، صفحه‌ها همیشه در حالت ایده‌آل نیستند.گاهی داده هنوز نرسیده است. گاهی کوئری با خطا روبه‌رو می‌شود. گاهی هم اصلاً محتوایی برای یک slug وجود ندارد.

loading، error و not-found در Next.js

بعد از اینکه داده واقعی را وارد صفحه‌ها می‌کنیم، یک مسئله مهم خیلی زود خودش را نشان می‌دهد:
صفحه‌ها همیشه در حالت ایده‌آل نیستند.

گاهی داده هنوز نرسیده است.
گاهی کوئری با خطا روبه‌رو می‌شود.
گاهی هم اصلاً محتوایی برای یک slug وجود ندارد.

در Next.js با App Router، این حالت‌ها با چند convention مشخص مدیریت می‌شوند:

  • loading.tsx
  • error.tsx
  • not-found.tsx

این فایل‌ها فقط جزئیات اضافه نیستند. در عمل، بخشی از معماری واقعی صفحه هستند.
اگر آن‌ها را از ابتدا درست طراحی کنی، UX پروژه بسیار طبیعی‌تر، حرفه‌ای‌تر و قابل نگهداری‌تر می‌شود.


چرا این stateها مهم‌اند؟

وقتی پروژه هنوز با mock data کار می‌کند، همه‌چیز بیش از حد تمیز به‌نظر می‌رسد.
آرایه آماده است، خطایی وجود ندارد و همیشه چیزی برای نمایش هست.

اما در پروژه واقعی این فرض‌ها درست نیستند.

مثلاً در یک پرتفولیو یا CMS:

  • لیست پروژه‌ها ممکن است هنوز در حال دریافت باشد
  • کوئری دیتابیس ممکن است موقتاً fail شود
  • کاربر ممکن است به slugای برود که وجود ندارد
  • یک بخش از صفحه ممکن است آماده باشد و بخش دیگر هنوز در حال load شدن باشد

اگر این وضعیت‌ها را مدیریت نکنی، نتیجه معمولاً یکی از این‌ها می‌شود:

  • صفحه‌ای که ناگهان خالی می‌ماند
  • خطای خام و نامناسب برای کاربر
  • تجربه‌ای نامشخص هنگام navigation
  • نبود مسیر روشن برای retry یا بازگشت

برای همین، loading، error و not-found باید بخشی از طراحی واقعی routeها باشند، نه چیزی که در پایان کار به پروژه اضافه می‌شود.


ساخت loading.tsx

در App Router، اگر داخل یک route segment فایلی به نام loading.tsx بسازیم، Next.js آن را به‌عنوان fallback همان segment استفاده می‌کند.

مثلاً:

// app/projects/loading.tsx
export default function Loading() {
  return <p>Loading projects...</p>
}

به‌محض اینکه کاربر وارد این route شود و محتوای آن هنوز آماده نباشد، این UI نمایش داده می‌شود و بعد از کامل شدن رندر، محتوای اصلی جای آن را می‌گیرد.

نکته مهم این است که loading.tsx در عمل با Suspense کار می‌کند و برای همان segment یک loading state خودکار می‌سازد.

این فایل دقیقاً کجا اثر می‌گذارد؟

در همان segment، loading.tsx داخل layout.tsx قرار می‌گیرد و page.tsx و فرزندانش را پوشش می‌دهد.
اما یک نکته مهم در داک رسمی این است که اگر خود layout.tsx به داده uncached یا runtime access وابسته باشد، loading.tsx لزوماً fallback آن را نمایش نمی‌دهد.

در عمل، یعنی اگر می‌خواهی navigation سریع‌تر و fallback قابل‌اعتمادتر باشد:

  • دریافت داده سنگین را تا جای ممکن به page.tsx منتقل کنیم
  • یا بخش‌های کند را با Suspense جداگانه بپوشانیم

loading.tsx را کجا استفاده کنیم؟

برای routeهایی مثل این‌ها:

  • app/projects/loading.tsx
  • app/blog/loading.tsx
  • app/dashboard/loading.tsx

این فایل معمولاً مناسب است وقتی می‌خواهیم برای کل route یک fallback اولیه داشته باشیم.


skeleton loading

از نظر فنی، loading.tsx می‌تواند فقط یک متن ساده برگرداند.
اما در UI واقعی، معمولاً بهتر است به‌جای Loading... از skeleton استفاده کنیم.

مثال ساده:

// app/projects/loading.tsx
export default function Loading() {
  return (
    <section className="grid gap-6 md:grid-cols-2">
      {Array.from({ length: 4 }).map((_, index) => (
        <div
          key={index}
          className="h-48 animate-pulse rounded-md border border-white/10 bg-white/5"
        />
      ))}
    </section>
  )
}

چرا این مدل بهتر است؟

  • کاربر سریع‌تر می‌فهمد چه چیزی در حال load شدن است
  • layout صفحه کمتر دچار پرش می‌شود
  • تجربه کلی حرفه‌ای‌تر به‌نظر می‌رسد

برای صفحه پروژه‌ها، skeleton کارت پروژه معمولاً از spinner یا متن ساده بهتر است.
برای بلاگ، skeleton لیست پست‌ها یا محتوای article منطقی‌تر است.
یعنی loading state باید تا حد ممکن شبیه ساختار نهایی همان صفحه باشد.


ساخت error.tsx

وقتی یک خطای غیرمنتظره داخل route رخ می‌دهد، Next.js می‌تواند با error.tsx یک fallback UI نمایش دهد.

مثلاً:

// app/projects/error.tsx
'use client'

import { useEffect } from 'react'

export default function Error({
  error,
  unstable_retry,
}: {
  error: Error & { digest?: string }
  unstable_retry: () => void
}) {
  useEffect(() => {
    console.error(error)
  }, [error])

  return (
    <section className="space-y-4">
      <h2>Something went wrong</h2>
      <p>We could not load the projects right now.</p>
      <button onClick={() => unstable_retry()}>Try again</button>
    </section>
  )
}

چند نکته مهم:

  • error.tsx باید Client Component باشد، پس به 'use client' نیاز دارد
  • این فایل نقش Error Boundary را برای همان segment بازی می‌کند
  • با unstable_retry() می‌توان تلاش دوباره برای render و fetch انجام داد

error.tsx چه خطاهایی را می‌گیرد؟

این فایل برای uncaught exceptions است؛ یعنی خطاهای غیرمنتظره‌ای که نباید جزو جریان عادی برنامه باشند.
مثلاً:

  • خطای runtime در render
  • شکست غیرمنتظره در لایه داده
  • باگ در منطق یک کامپوننت

اگر خطا از یکی از فرزندان همین segment رخ دهد، نزدیک‌ترین error.tsx آن را می‌گیرد.
این رفتار باعث می‌شود بتوانی error handling را به‌صورت granular در سطوح مختلف route hierarchy تعریف کنی.

یک نکته مهم درباره production

طبق داک رسمی، خطاهایی که از Server Component به client فرستاده می‌شوند در production معمولاً پیام خام سرور را نشان نمی‌دهند.
به‌جای آن، یک پیام عمومی و digest داده می‌شود تا جزئیات حساس نشت نکند.

پس بهتر است UI خطا را بر پایه پیام‌های عمومی و انسانی طراحی کنی، نه بر اساس نمایش مستقیم error.message.


پیام‌های خطا

یک اشتباه رایج این است که error.tsx فقط چیزی شبیه Something went wrong نشان دهد و تمام.
این بهتر از crash کامل است، اما برای UX کافی نیست.

پیام خطا باید:

  • کوتاه باشد
  • روشن باشد
  • از نظر لحن با بقیه محصول هماهنگ باشد
  • کار بعدی کاربر را مشخص کند

مثلاً برای صفحه پروژه‌ها:

  • We could not load the projects right now.
  • Please try again in a moment.

برای فرم‌ها یا server actionها هم باید بین خطای مورد انتظار و خطای غیرمنتظره تفاوت بگذاریم.

طبق داک رسمی، در خطاهای مورد انتظار، مخصوصاً در Server Functions، بهتر است به‌جای throw کردن، نتیجه خطا را به‌عنوان return value برگردانیم و در UI نمایش بدهیم.

مثال مفهومی:

'use server'

export async function createPost(prevState: any, formData: FormData) {
  const title = formData.get('title')

  if (!title) {
    return { message: 'Title is required.' }
  }

  return { message: '' }
}

و بعد در فرم:

'use client'

import { useActionState } from 'react'
import { createPost } from '@/app/actions'

const initialState = {
  message: '',
}

export function PostForm() {
  const [state, formAction, pending] = useActionState(createPost, initialState)

  return (
    <form action={formAction}>
      <input name="title" />
      {state.message ? <p aria-live="polite">{state.message}</p> : null}
      <button disabled={pending}>Create</button>
    </form>
  )
}

این بخش مهم است، چون همه خطاها قرار نیست به error.tsx بروند.
بعضی خطاها بخشی از رفتار عادی سیستم هستند و باید مستقیم در UI مدیریت شوند.


ساخت not-found.tsx

وقتی route پیدا شده، اما منبعی که صفحه به آن وابسته است وجود ندارد، باید از notFound() استفاده کنیم و UI مناسب را در not-found.tsx نمایش بدهیم.

مثلاً در صفحه جزئیات یک پست:

// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'
import { getPostBySlug } from '@/lib/data/posts'

export default async function PostPage({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const post = await getPostBySlug(slug)

  if (!post) {
    notFound()
  }

  return <article>{post.title}</article>
}

و بعد:

// app/blog/[slug]/not-found.tsx
import Link from 'next/link'

export default function NotFound() {
  return (
    <section className="space-y-4">
      <h1>Post not found</h1>
      <p>The requested article does not exist.</p>
      <Link href="/blog">Back to blog</Link>
    </section>
  )
}

چه زمانی از notFound() استفاده کنیم؟

وقتی:

  • slug معتبر نیست
  • رکوردی در دیتابیس پیدا نشده
  • محتوای publish نشده نباید برای route عمومی نمایش داده شود

این حالت با error.tsx فرق دارد.
در notFound() مشکلی در اجرای برنامه رخ نداده؛ فقط منبع موردنظر وجود ندارد یا قابل نمایش نیست.

نکته مهم درباره status code

طبق داک رسمی، در حالت streaming ممکن است پاسخ نهایی با status 200 برگردد، حتی اگر محتوای not-found نمایش داده شود.
در این شرایط Next.js از noindex برای جلوگیری از ایندکس شدن URL استفاده می‌کند.

برای بیشتر پروژه‌های محتوایی این رفتار مشکلی ایجاد نمی‌کند، اما خوب است بدانیم که در routeهای stream‌شده، نمایش 404 UI همیشه به‌معنای 404 HTTP status نیست.


تفاوت loading، error و not-found

اگر بخواهیم خیلی خلاصه فرق این سه state را روشن کنیم:

  • loading.tsx: وقتی داده یا UI هنوز آماده نشده
  • error.tsx: وقتی خطای غیرمنتظره باعث شکست render یا fetch شده
  • not-found.tsx: وقتی route یا resource موردنظر وجود ندارد

این تفکیک مهم است، چون هرکدام معنای متفاوتی در UX دارند.

مثلاً برای یک صفحه پروژه با slug:

  • اگر داده هنوز در حال دریافت است: loading
  • اگر query با خطای غیرمنتظره fail شود: error
  • اگر پروژه‌ای با آن slug وجود نداشته باشد: not-found

جمع‌بندی

در Next.js با App Router، مدیریت stateهای واقعی صفحه فقط به data fetching ختم نمی‌شود.
برای اینکه routeها حرفه‌ای و قابل‌اعتماد باشند، باید از conventionهای خود framework استفاده کنیم:

  • loading.tsx برای نمایش fallback هنگام load شدن route
  • error.tsx برای خطاهای غیرمنتظره و recovery با unstable_retry()
  • not-found.tsx برای زمانی که منبع موردنظر وجود ندارد
  • و skeletonها برای loading UI معنادارتر

نتیجه این کار فقط بهتر شدن ظاهر صفحه نیست.
در عمل، ساختار routeها خواناتر می‌شود، UX طبیعی‌تر می‌شود و پروژه برای رشد بعدی آماده‌تر می‌ماند.

// 0 comments
#Next.js#آموزش

نظرات (0)

نظر خود را بنویسید