searchParams و Query String
در درس قبل بخشی از مسیر آدرس را پویا کردیم (مثل /posts/hello-nextjs). اما بسیاری از حالتهای صفحه به مسیر مربوط نیستند، بلکه «تنظیمات نمایش» هستند: عبارت جستوجو، فیلتر برچسب، شمارهی صفحه یا ترتیب مرتبسازی. اینها معمولاً بعد از علامت ? در آدرس میآیند و Query String نام دارند. در این درس یاد میگیریم Query String را در Next.js 16 با searchParams بخوانیم و جستوجوی سادهی DevNotes را شروع کنیم.
Query String چیست؟
Query String بخشی از آدرس است که بعد از ? میآید و از جفتهای کلید=مقدار تشکیل میشود که با & از هم جدا میشوند:
/posts?q=nextjs&page=2&tag=react&tag=cache
└──────────────┬───────────────┘
Query String| بخش | مقدار | توضیح |
|---|---|---|
q=nextjs |
"nextjs" |
یک کلید با یک مقدار |
page=2 |
"2" |
مقدار همیشه رشته است، نه عدد |
tag=react&tag=cache |
["react", "cache"] |
کلید تکراری به آرایه تبدیل میشود |
مزیت مهم Query String این است که حالت صفحه داخل آدرس است؛ پس قابل اشتراکگذاری، قابل ذخیرهی نشانک (Bookmark) و سازگار با دکمهی «بازگشت» مرورگر است.
params یا searchParams؟
| ویژگی | params |
searchParams |
|---|---|---|
| منبع | قطعههای پویای مسیر ([slug]) |
Query String بعد از ? |
| وجود در ساختار پوشهها | بله، با پوشهی [slug] |
خیر، به پوشهای نیاز ندارد |
| الزامی بودن | بدون آن مسیر تطبیق نمیکند | اختیاری است؛ ممکن است وجود نداشته باشد |
| نوع در Next.js 16 | Promise | Promise |
| کجا در دسترس است؟ | صفحهها و Layoutها | فقط صفحهها (page.tsx) |
یک قاعدهی عملی: چیزی که «هویت» صفحه را مشخص میکند (مثل کدام نوشته) در مسیر میرود و چیزی که «نحوهی نمایش» را تغییر میدهد (جستوجو، فیلتر، صفحهبندی) در Query String.
خواندن searchParams در صفحه
هر page.tsx علاوه بر params، prop دیگری به نام searchParams هم میگیرد. در Next.js 16 این prop هم یک Promise است و باید await شود:
type PostsPageProps = {
searchParams: Promise<{
[key: string]: string | string[] | undefined;
}>;
};
export default async function PostsPage({ searchParams }: PostsPageProps) {
const params = await searchParams;
return <p>مقدار q: {String(params.q)}</p>;
}- نوع هر مقدار
string | string[] | undefinedاست: یک رشته، آرایهای از رشتهها (کلید تکراری) یا نبودِ کلید. - برخلاف
params، مقدارهایsearchParamsمعمولاً قبلاً دیکد شدهاند و فارسی را به شکل متن معمولی میگیرید. - این مقدار فقط در صفحه در دسترس است؛
layout.tsxهیچوقتsearchParamsنمیگیرد. دلیلش این است که Layout هنگام جابهجایی بین صفحات دوباره رندر نمیشود و ممکن بود مقدار قدیمی نشان دهد.
ساخت فرم جستوجو بدون JavaScript
سادهترین راه تغییر Query String یک فرم HTML با متد پیشفرض GET است. وقتی فرم ارسال شود، مرورگر مقدار فیلدها را به شکل Query String به آدرس action اضافه میکند. هیچ کد کلاینتی لازم نیست و فرم داخل یک Server Component کار میکند.
<form action="/posts">
<label htmlFor="q">جستوجو</label>
<input id="q" type="search" name="q" />
<button type="submit">بجو</button>
</form>
اگر کاربر در فیلد، nextjs را بنویسد و دکمه را بزند، مرورگر به /posts?q=nextjs میرود. ویژگی name نام کلید در Query String را تعیین میکند.
عملی: جستوجوی صفحهی نوشتههای DevNotes
فایل app/(public)/posts/page.tsx را به این شکل بهروزرسانی کنید. هنوز دادهی واقعی نداریم، پس فقط مقدارها را میخوانیم، اعتبارسنجی میکنیم و نمایش میدهیم. فیلتر واقعی را وقتی داده داریم اضافه میکنیم.
فایلهای متأثر: فقط app/(public)/posts/page.tsx.
// app/(public)/posts/page.tsx
import Link from "next/link";
import PostsIntro from "./_components/PostsIntro";
type PostsPageProps = {
searchParams: Promise<{
[key: string]: string | string[] | undefined;
}>;
};
// اگر کلیدی تکراری بود فقط اولین مقدار را برمیداریم.
function first(value: string | string[] | undefined): string | undefined {
return Array.isArray(value) ? value[0] : value;
}
// ساخت آدرس با Query String تمیز (بدون کلید خالی).
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 />
<form action="/posts">
<label htmlFor="q">جستوجو در نوشتهها</label>
<input id="q" type="search" name="q" defaultValue={q} />
<button type="submit">بجو</button>
</form>
<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>
);
}توضیح کد
await searchParams: آبجکت Query String را میگیرد. چون Promise است، تابعasyncشده است.first: چون هر مقدار میتواند رشته، آرایه یاundefinedباشد، این تابع کمکی آن را به یک رشته (یاundefined) تبدیل میکند. مثلاً اگر کسی آدرس?q=a&q=bرا بزند، فقط"a"را میگیریم.?.trim() ?? "": فاصلههای اضافه را حذف میکند و اگر کلید نبود، رشتهی خالی میدهد.page: مقدار رشتهای را به عدد تبدیل میکنیم و فقط عدد صحیح بزرگتر یا مساوی ۱ را میپذیریم؛ در غیر این صورت صفحهی ۱. یعنی ورودیهایی مثل?page=abcیا?page=-5مشکلی ایجاد نمیکنند.defaultValue={q}: مقدار جستوجوی قبلی بعد از ارسال فرم در فیلد باقی میماند.buildHrefباURLSearchParamsآدرس را میسازد و خودش کاراکترهای خاص را کدگذاری میکند. کلیدهای خالی (مثلq=) را اضافه نمیکند.- متن جستوجو را داخل JSX قرار دادهایم؛ React مقدار را خودکار Escape میکند، پس ورودی کاربر بهعنوان HTML اجرا نمیشود.
خروجی و رفتار مورد انتظار
/posts: متن «همهی نوشتهها — صفحهی 1» و فقط لینک «صفحهی بعد».- نوشتن
nextjsدر فرم و ارسال: آدرس به/posts?q=nextjsمیرود و متن «نتیجهی جستوجو برای «nextjs» — صفحهی 1» دیده میشود. - کلیک روی «صفحهی بعد»: آدرس
/posts?q=nextjs&page=2میشود و لینک «صفحهی قبل» هم ظاهر میشود. /posts?page=abcو/posts?page=0: هر دو صفحهی ۱ را نشان میدهند.- دکمهی «بازگشت» مرورگر بین حالتهای مختلف جستوجو درست کار میکند، چون هر حالت یک آدرس دارد.
خواندن Query String در Client Component
prop searchParams فقط در صفحهی Server Component در دسترس است. اگر Client Componentی (مثلاً نوار جستوجو در هدر) لازم داشته باشد Query String را بخواند، از Hook با نام useSearchParams در next/navigation استفاده میکند:
// components/SearchSummary.tsx
"use client";
import { useSearchParams } from "next/navigation";
export default function SearchSummary() {
const searchParams = useSearchParams();
const q = searchParams.get("q") ?? "";
return <p>عبارت فعلی: {q || "—"}</p>;
}searchParams.get("q")اولین مقدار کلید را برمیگرداند (یاnull)؛ برای کلیدهای تکراریgetAllوجود دارد.- این Hook فقط خواندن را انجام میدهد. تغییر آدرس از داخل کد با
useRouterدر درس «مسیریابی برنامهای با useRouter» میآید. - اگر کامپوننتی که
useSearchParamsدارد در یک مسیر استاتیک استفاده شود، باید داخل یک مرزSuspenseقرار بگیرد؛ در غیر این صورت Build خطا میدهد.Suspenseرا در فصل «Loading، Error و Streaming» میشناسیم. ماSearchSummaryرا در DevNotes استفاده نمیکنیم و این نمونه فقط برای شناخت است.
searchParams و نوع رندر صفحه
Query String فقط هنگام درخواست مشخص میشود؛ پس صفحهای که searchParams را میخواند نمیتواند هنگام Build برای همهی مقادیر ممکن از پیش ساخته شود. در حالت پیشفرض (بدون cacheComponents) چنین صفحهای هنگام درخواست رندر میشود (Dynamic Rendering). این به معنی «بدون کش» نیست؛ بین رندر پویا و کش داده تفاوت هست و در درسهای «ارتباط Cache با Static Rendering، Dynamic Rendering و APIهای پویای درخواست» و «مدلهای رندر: Static، Dynamic، SSR، SSG و ISR» دقیق بررسی میشود. اگر cacheComponents روشن باشد، دسترسی به searchParams هم قواعد خاص خودش را دارد که در فصل کش میآید.
نکتههای مهم
- همهی مقدارها رشتهاند: قبل از استفادهی عددی، تبدیل و اعتبارسنجی کنید، همانطور که با
pageانجام دادیم. - به ورودی آدرس اعتماد نکنید: هر کسی میتواند هر Query Stringی تایپ کند. مقدارها را اعتبارسنجی کنید و هنگام استفاده در پایگاه داده، از پرسوجوی پارامتری استفاده کنید (در فصل «پایگاه داده با Prisma و SQLite»).
- Layout هرگز searchParams نمیگیرد: اگر Layout به Query String نیاز دارد، باید آن بخش را به Client Component با
useSearchParamsبسپارید. - کلید تکراری آرایه میشود: کدی که فقط رشته را فرض کند، با آدرسهای ساختهشدهی دستی میشکند؛ برای همین تابع
firstرا نوشتیم. - ورودی را مستقیماً به صورت HTML رندر نکنید: از
dangerouslySetInnerHTMLبرای مقدار Query String استفاده نکنید.
جمعبندی
- Query String بخش بعد از
?در آدرس است و برای حالتهای نمایش مثل جستوجو، فیلتر و صفحهبندی استفاده میشود. - در Next.js 16، prop به نام
searchParamsیک Promise است و فقط درpage.tsxوجود دارد؛ Layout آن را نمیگیرد. - مقدار هر کلید
string | string[] | undefinedاست و باید قبل از استفاده نرمالسازی و اعتبارسنجی شود. - یک فرم HTML با متد GET بدون JavaScript Query String میسازد و برای جستوجوی ساده کافی است.
- در Client Component از
useSearchParamsبرای خواندن استفاده میکنیم. - خواندن
searchParamsرندر صفحه را به زمان درخواست میبرد؛ رابطهی دقیقش با کش را در فصلهای بعد میبینیم. - در DevNotes اکنون صفحهی
/postsفرم جستوجو و صفحهبندی ساده دارد. - در درس بعد، «Catch-all و Optional Catch-all Routes»، مسیرهایی میسازیم که چند بخش از آدرس را یکجا میگیرند.
