مسیریابی برنامهای با useRouter
تا اینجا هر جابهجایی بین صفحات با کلیک روی یک لینک (Link) انجام میشد. اما گاهی باید از داخل کد به صفحهی دیگری برویم: مثلاً بعد از موفقیت ورود، بعد از ذخیرهی یک فرم، با دکمهی «بازگشت» یا هنگام جستوجوی زنده. به این کار مسیریابی برنامهای (Programmatic Navigation) میگویند و ابزار آن در App Router، Hook با نام useRouter است. در این درس متدهای آن را میشناسیم، تفاوتش را با Link و redirect میفهمیم و دو کامپوننت واقعی برای DevNotes میسازیم.
useRouter چیست؟
useRouter یک Hook است که آبجکت Router را برمیگرداند. با آن میتوان بدون کلیک روی لینک، ناوبری را از کد انجام داد. نکتههای اصلی:
- باید از
next/navigationوارد شود، نهnext/routerکه مخصوص Pages Router است. وارد کردن از مسیر اشتباه در App Router خطا میدهد. - فقط در Client Component کار میکند، یعنی فایل باید با
"use client"شروع شود. دلیلش این است که ناوبری یک عمل سمت مرورگر است. - برای ناوبری معمولی بین صفحات، خود
Linkبهتر است؛useRouterبرای وقتی است که ناوبری نتیجهی یک رویداد یا منطق برنامه باشد.
"use client";
import { useRouter } from "next/navigation";
export default function Example() {
const router = useRouter();
// router.push, router.replace, router.back ...
return null;
}متدهای اصلی Router
| متد | کاربرد |
|---|---|
router.push(href) |
رفتن به یک آدرس و افزودن یک رکورد جدید به تاریخچهی مرورگر؛ دکمهی «بازگشت» به صفحهی قبل برمیگردد. |
router.replace(href) |
رفتن به یک آدرس با جایگزینکردن رکورد فعلی تاریخچه؛ «بازگشت» به آدرس قبلی نمیرود. |
router.back() |
یک قدم به عقب در تاریخچه (مثل دکمهی «بازگشت» مرورگر). |
router.forward() |
یک قدم به جلو در تاریخچه. |
router.refresh() |
درخواست دوبارهی دادهی مسیر فعلی از سرور و بهروزرسانی Server Componentها، بدون از دست رفتن State کامپوننتهای کلاینتی. |
router.prefetch(href) |
بارگیری پیشاپیش یک مسیر برای ناوبری سریعتر (کاربرد محدود؛ جزئیات در درس «Prefetching و رفتار Navigation»). |
push در برابر replace
تفاوت این دو در تاریخچهی مرورگر است. فرض کنید کاربر از /posts به /login رفته و پس از ورود به /dashboard فرستاده میشود:
- با
push، تاریخچه چنین میشود:/posts←/login←/dashboard. با «بازگشت» دوباره صفحهی ورود دیده میشود، که معمولاً مطلوب نیست. - با
replace، رکورد/loginبا/dashboardجایگزین میشود:/posts←/dashboard.
قاعدهی عملی: جابهجاییهای عادی با push، و جابهجاییهایی که نباید در تاریخچه بمانند (بعد از ورود، یا بهروزرسانی پیاپی Query String هنگام تایپ) با replace.
گزینهی scroll
push و replace یک آرگومان دوم اختیاری دارند. بهطور پیشفرض بعد از ناوبری به بالای صفحه میرویم. با scroll: false میتوان آن را خاموش کرد:
router.push("/posts?page=2", { scroll: false });سه راه رفتن به صفحهی دیگر: کدام را کِی؟
| ابزار | کجا کار میکند | مناسب برای |
|---|---|---|
Link |
Server و Client Component | لینکهایی که کاربر روی آنها کلیک میکند (ناوبری، منو، فهرست) |
useRouter |
فقط Client Component | ناوبری بعد از رویداد یا منطق سمت مرورگر (بازگشت، جستوجوی زنده، بعد از پاسخ موفق) |
redirect() |
Server Component، Server Action و Route Handler | هدایت در سمت سرور، قبل از ارسال صفحه (در درس «Redirect، not-found.tsx و notFound()») |
اگر میتوانید با یک Link کار را انجام دهید، همان بهتر است: یک تگ <a> واقعی میسازد، برای SEO و دسترسپذیری خوب است و با کلیک میانی و «باز کردن در تب جدید» هم درست کار میکند. router.push هیچکدام از اینها را ندارد.
عملی ۱: دکمهی بازگشت
یک کامپوننت کوچک میسازیم که با router.back() به صفحهی قبلی برمیگردد. چون Hook لازم دارد، Client Component است.
// components/BackButton.tsx
"use client";
import { useRouter } from "next/navigation";
type BackButtonProps = {
label?: string;
};
export default function BackButton({ label = "بازگشت" }: BackButtonProps) {
const router = useRouter();
return (
<button type="button" onClick={() => router.back()}>
{label}
</button>
);
}type="button"باعث میشود اگر داخل یک فرم قرار گرفت، فرم را ارسال نکند.labelیک prop اختیاری با مقدار پیشفرض است تا بتوان متن را عوض کرد.
حالا آن را در صفحهی جزئیات نوشته استفاده میکنیم. چون صفحه Server Component است و فقط BackButton در مرورگر اجرا میشود، این ترکیب کاملاً مجاز است (جزئیات در فصل «Server Components و Client Components»).
// app/(public)/posts/[slug]/page.tsx
import BackButton from "@/components/BackButton";
type PostPageProps = {
params: Promise<{ slug: string }>;
};
export async function generateStaticParams() {
return [
{ slug: "hello-nextjs" },
{ slug: "app-router-intro" },
];
}
export default async function PostPage({ params }: PostPageProps) {
const { slug } = await params;
return (
<main>
<BackButton />
<h1>نوشته: {slug}</h1>
<p>محتوای این نوشته بعد از اتصال به داده نمایش داده میشود.</p>
</main>
);
}
اگر کاربر مستقیم با وارد کردن آدرس وارد این صفحه شده باشد و تاریخچهای نداشته باشد، back() او را از سایت بیرون میبرد یا کاری نمیکند. برای جایی که مقصد بازگشت باید قطعی باشد، Link مطمئنتر است.
عملی ۲: جعبهی جستوجو با useRouter
در درس «searchParams و Query String» جستوجو را با یک فرم GET ساده ساختیم. اینجا همان را با router.push میسازیم تا الگوی ناوبری از داخل کد را ببینید. این روش در جستوجوی زنده و فرمهای پیچیدهتر کاربرد دارد.
فایلهای متأثر: فایل جدید components/SearchBox.tsx و تغییر app/(public)/posts/page.tsx (فرم قبلی با این کامپوننت جایگزین میشود).
// components/SearchBox.tsx
"use client";
import { useState, type FormEvent } from "react";
import { useRouter } from "next/navigation";
type SearchBoxProps = {
defaultValue?: string;
};
export default function SearchBox({ defaultValue = "" }: SearchBoxProps) {
const router = useRouter();
const [value, setValue] = useState(defaultValue);
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const q = value.trim();
const search = new URLSearchParams();
if (q) search.set("q", q);
const queryString = search.toString();
router.push(queryString ? `/posts?${queryString}` : "/posts");
}
return (
<form role="search" onSubmit={handleSubmit}>
<label htmlFor="q">جستوجو در نوشتهها</label>
<input
id="q"
type="search"
value={value}
onChange={(event) => setValue(event.target.value)}
/>
<button type="submit">بجو</button>
</form>
);
}// app/(public)/posts/page.tsx
import Link from "next/link";
import PostsIntro from "./_components/PostsIntro";
import SearchBox from "@/components/SearchBox";
type PostsPageProps = {
searchParams: Promise<{
[key: string]: string | string[] | undefined;
}>;
};
function first(value: string | string[] | undefined): string | undefined {
return Array.isArray(value) ? value[0] : value;
}
function buildHref(q: string, page: number): string {
const search = new URLSearchParams();
if (q) search.set("q", q);
if (page > 1) search.set("page", String(page));
const queryString = search.toString();
return queryString ? `/posts?${queryString}` : "/posts";
}
export default async function PostsPage({ searchParams }: PostsPageProps) {
const params = await searchParams;
const q = first(params.q)?.trim() ?? "";
const rawPage = Number(first(params.page));
const page = Number.isInteger(rawPage) && rawPage >= 1 ? rawPage : 1;
return (
<main>
<h1>نوشتهها</h1>
<PostsIntro />
<SearchBox key={q} defaultValue={q} />
<p>
{q ? `نتیجهی جستوجو برای «${q}»` : "همهی نوشتهها"} — صفحهی {page}
</p>
<nav aria-label="صفحهبندی">
{page > 1 && (
<Link href={buildHref(q, page - 1)}>صفحهی قبل</Link>
)}{" "}
<Link href={buildHref(q, page + 1)}>صفحهی بعد</Link>
</nav>
</main>
);
}توضیح کد
"use client"درSearchBoxلازم است چون همuseStateو همuseRouterدارد.event.preventDefault()جلوی ارسال معمولی فرم و بارگذاری کامل صفحه را میگیرد تا خودمان ناوبری را انجام دهیم.router.pushآدرس را عوض میکند. چون صفحهی/postssearchParamsرا میخواند، Next.js آن را با Query String جدید دوباره رندر میکند.- صفحهی
PostsPageهمچنان Server Component است؛ مقدارqرا ازsearchParamsمیخواند و از طریق prop بهSearchBoxمیدهد. به این ترتیب لازم نیست در Client Component ازuseSearchParamsاستفاده کنیم. key={q}یک نکتهی مهم دارد:useState(defaultValue)فقط مقدار اولیه را میگیرد. اگر کاربر با دکمهی «بازگشت» مرورگر آدرس را عوض کند،defaultValueتغییر میکند ولی State قدیمی میماند. با دادنkey، هر بار کهqعوض شود، React کامپوننت را از نو میسازد و مقدار فیلد با آدرس هماهنگ میشود.- این فرم بدون JavaScript کار نمیکند، در حالی که فرم GET قبلی بدون JavaScript هم کار میکرد. برای جستوجوی ساده، فرم GET سادهتر و مقاومتر است؛ ما اینجا برای آموزش از
useRouterاستفاده کردیم.
خروجی و رفتار مورد انتظار
- در
/postsعبارتی بنویسید و دکمه را بزنید: آدرس به/posts?q=...تغییر میکند و متن نتیجه بهروز میشود، بدون بارگذاری کامل صفحه. - فیلد را خالی بفرستید: آدرس به
/postsبرمیگردد. - بعد از چند جستوجو، دکمهی «بازگشت» مرورگر را بزنید: آدرس و مقدار داخل فیلد هر دو به حالت قبل برمیگردند.
- در صفحهی
/posts/hello-nextjsدکمهی «بازگشت» شما را به صفحهی قبلی میبرد.
جستوجوی زنده با replace (فقط برای شناخت)
اگر بخواهیم با هر بار تایپ، نتیجه بهروز شود، نباید با هر کاراکتر یک رکورد تاریخچه ساخته شود. در این حالت از replace و یک تأخیر کوتاه (Debounce) استفاده میکنند:
// نمونهی مفهومی؛ در DevNotes اعمال نمیشود
useEffect(() => {
const timer = setTimeout(() => {
const q = value.trim();
router.replace(q ? `/posts?q=${encodeURIComponent(q)}` : "/posts", {
scroll: false,
});
}, 400);
return () => clearTimeout(timer);
}, [value, router]);setTimeoutتا ۴۰۰ میلیثانیه بعد از آخرین تایپ صبر میکند وclearTimeoutتایمر قبلی را لغو میکند.replaceمانع شلوغشدن تاریخچه میشود.scroll: falseصفحه را هنگام تایپ به بالا نمیبرد.
router.refresh چه کاری میکند؟
router.refresh() مسیر فعلی را دوباره از سرور میگیرد و Server Componentهای آن را بهروز میکند، در حالی که State کامپوننتهای کلاینتی (مثلاً متن داخل یک فیلد) حفظ میشود. ممکن است فکر کنید این همان بازاعتبارسنجی کش است، ولی این دو یکی نیستند: refresh سمت کلاینت فقط Router را تازه میکند و دادهی کششدهی سرور را لزوماً منقضی نمیکند. تفاوت دقیق آن با revalidatePath، revalidateTag و refresh سمت سرور را در درس «refresh، بهروزرسانی رابط کاربری و تفاوت آن با Revalidation» میبینیم.
نکتههای مهم
-
import درست:
useRouterرا ازnext/navigationبگیرید. نسخهیnext/routerفقط در Pages Router کار میکند و در App Router خطا میدهد. -
Router رویداد ندارد: آبجکت Router در App Router رویدادهایی مثل
router.eventsندارد. برای واکنش به تغییر مسیر، ازusePathnameوuseSearchParamsاستفاده میشود. -
آدرس را اعتبارسنجی کنید: اگر مقصد ناوبری از ورودی کاربر یا Query String میآید (مثلاً
?next=/dashboard)، قبل از استفاده بررسی کنید که فقط آدرس داخلی سایت باشد، وگرنه کاربر میتواند به سایت دیگری هدایت شود. این موضوع را در فصل «Authentication و Security» دوباره میبینیم. -
Router ناوبری سرور را جایگزین نمیکند: هدایتهایی که باید همیشه رخ دهند (مثل «اگر وارد نشدهای برو به ورود») در سمت سرور انجام میشوند، نه با
useRouter؛ چون کد کلاینت قابل دور زدن است. -
برای لینکهای معمولی Link: اگر برای یک منو از
onClickوrouter.pushاستفاده کنید، دسترسپذیری و رفتار مرورگر (مثل کلیک راست و باز کردن در تب جدید) از دست میرود.
جمعبندی
useRouterازnext/navigationبرای ناوبری از داخل کد است و فقط در Client Component کار میکند.- متدهای اصلی:
push(رکورد جدید)،replace(جایگزینی رکورد)،back،forward،refreshوprefetch. - برای کلیک کاربر روی لینک
Link، برای ناوبری بعد از رویدادuseRouterو برای هدایت سمت سرورredirect()مناسب است. - برای هماهنگماندن State کامپوننت با آدرس میتوان از
keyاستفاده کرد. - در DevNotes دو کامپوننت
BackButtonوSearchBoxساختیم و صفحهی نوشتهها و جزئیات نوشته را بهروز کردیم. - در درس بعد، «Redirect، not-found.tsx و notFound()»، هدایت و مدیریت صفحهی پیدا نشد را در سمت سرور یاد میگیریم.
