Search params и query
Query-параметры (?page=2&sort=date) читаются через searchParams.
В серверном компоненте
export default async function Page({
searchParams,
}: {
searchParams: Promise<{ page?: string; sort?: string }>;
}) {
const { page = '1', sort = 'date' } = await searchParams;
return <p>Страница {page}, сортировка {sort}</p>;
}
searchParams — промис, нужно await.
Типы
Значения — строки или массивы строк:
// ?tag=js&tag=react
searchParams: Promise<{ tag?: string | string[] }>;
const { tag } = await searchParams;
const tags = Array.isArray(tag) ? tag : tag ? [tag] : [];
В клиентском компоненте
'use client';
import { useSearchParams } from 'next/navigation';
export default function Search() {
const searchParams = useSearchParams();
const page = searchParams.get('page') ?? '1';
return <p>Страница {page}</p>;
}
Обновление параметров
В серверном компоненте — через Link:
import Link from 'next/link';
<Link href="/posts?page=2">Страница 2</Link>
<Link href={{ pathname: '/posts', query: { page: 2 } }}>Страница 2</Link>
В клиентском — через useRouter:
'use client';
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
export default function Pagination() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
function setPage(page: number) {
const params = new URLSearchParams(searchParams);
params.set('page', String(page));
router.push(`${pathname}?${params}`);
}
return <button onClick={() => setPage(2)}>2</button>;
}
Кеш отключается
Использование searchParams делает страницу динамической — рендерится при каждом запросе, не на этапе сборки.
Это нормально, если нужны свежие данные.
Паттерн: фильтры
export default async function Posts({ searchParams }) {
const { search = '', status = 'all', page = '1' } = await searchParams;
const posts = await getPosts({
search,
status,
page: Number(page),
});
return (
<>
<Filters search={search} status={status} />
<PostList posts={posts} />
<Pagination page={Number(page)} />
</>
);
}
Компонент фильтров
'use client';
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
import { useDebouncedCallback } from 'use-debounce';
export function Filters({ search, status }) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const update = (key: string, value: string) => {
const params = new URLSearchParams(searchParams);
if (value) params.set(key, value);
else params.delete(key);
if (key !== 'page') params.set('page', '1');
router.push(`${pathname}?${params}`);
};
const debouncedSearch = useDebouncedCallback(
(value) => update('search', value),
300
);
return (
<>
<input
defaultValue={search}
onChange={(e) => debouncedSearch(e.target.value)}
/>
<select value={status} onChange={(e) => update('status', e.target.value)}>
<option value="all">Все</option>
<option value="published">Опубликованные</option>
</select>
</>
);
}
Ссылка с сохранением параметров
'use client';
import { useSearchParams } from 'next/navigation';
export function PaginationLink({ page }: { page: number }) {
const searchParams = useSearchParams();
const params = new URLSearchParams(searchParams);
params.set('page', String(page));
return <Link href={`/posts?${params}`}>{page}</Link>;
}
Итоги
searchParamsв серверных компонентахuseSearchParamsв клиентских- Все значения — строки
- Использование
searchParamsотключает статику - Фильтры и пагинация в URL
- Debounce для поиска