Урок 03

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 для поиска