Bash
website.com/blog?category=programming&tag=react
Сложно
Игорь Быстрицкий
Next
TypeScript
С помощью TypeScript ты сможешь явно указать все возможные параметры, используемые в URL страницы. Например:
website.com/blog?category=programming&tag=react
Из этого URL ты можешь понять, что тебе нужно читать значения двух поисковых параметров: category и tag. Также видно, что эта страница не имеет динамического пути.
Примерный тип этой страницы будет такой:
// Внимание на TNextPage.
type TBlogPage = TNextPage<never, ['tag', 'difficulty']>
Используя этот тип, ты получишь подсказки в редакторе (я надеюсь, это VSCode) и увидишь ошибки, если случайно обратишься к несуществующему параметру.
Сначала давай рассмотрим возможности, которые предоставляет Next и поговорим о странице.
Напоминаю, что страницей называется файл page.tsx
, который обязан экспортировать реакт компонент, имя которому ты придумываешь сам. Возьмем в качестве примера страницу новостей:
// page.tsx
export default function NewsPage() {
return <main>...</main>
}
Страница способна предоставить доступ к params
и searchParams
, которые были переданы как часть URL страницы.
Параметры - это последнее значение в пути до динамической страницы. В случае Next такая страница может находиться по пути app/user/[id]/page.tsx
. В данном случае [id]
является динамическим параметром и доступен на странице page.tsx
.
Получить его на странице можно следующим способом:
// page.tsx
export default function UserPage(pageProps) {
return <main>{pageProps.params.id}</main>
}
Однако в TypeScript такое не сработает, потому что VSCode не знает о типе pageProps
, поэтому у тебя нет выбора - нужно типизировать. И как ты это собираешься делать - уже новый вопрос.
Можно пойти по простому пути и типизировать объектом напрямую:
// page.tsx
export default function UserPage(pageProps: { params: { id: string } }) {
const id = pageProps.params.id
// id = 5
return <main>...</main>
}
Вау, это сработало! Но из-за того, что тебе надо повторять эту запись на каждой новой странице, придется позаботиться о будущем и вынести эту запись в отдельный тип.
// utils/index.ts
export type TNextPage = {
params: { id: string }
}
// user/[id]/page.tsx
export default function UserPage(pageProps: TNextPage) {
return <main>{pageProps.params.id}</main>
}
Просто супер! Но вот новая проблема: не все динамические страницы имеют путь с названием /[id]/
, также может быть и /[slug]/
, а может ты стремишься использовать внятные имена в проекте, поэтому укажешь в пути /[userId]/
. В таком случае нужно типизировать уже с помощью дженерика и перезаписывать стандартный id.
// utils/index.ts
// Дженерик 'P' и его стандартное значение 'id', также нужно ограничить его тип до строки используя ключ. слово 'extends', потому что все, что находится в URL удобнее обрабатывать как строку.
export type TNextPage<P extends string = 'id'> = {
params: Record<P, string>
}
// Тут без изменений, дженерик передавать не нужно, потому что мы используем стандартное имя 'id'.
// user/[id]/page.tsx
export default function UserPage(pageProps: TNextPage) {
return <main>{pageProps.params.id}</main>
}
// А тут нужно перезаписать с 'id' на 'articleSlug'.
// blog/[articleSlug]/page.tsx
// blog/kak-stat-krutim
export default function UserPage(pageProps: TNextPage<'articleSlug'>) {
const slug = pageProps.params.articleSlug
// slug = 'kak-stat-krutim'
return <main>...</main>
}
С Params закончили, теперь к давай самому сложному и противному - поисковые параметры (далее: ПП).
В URL страницы он может быть один:
website.com/blog?tag=next
Может отсутствовать (оба tag
и difficulty
будут null
):
website.com/blog
website.com/blog?
А может быть и несколько:
website.com/blog?tag=next&difficulty=easy
В последнем примере URL использует ?
чтобы проверить наличие страницы blog
и применить ПП. Каждый ПП имеет структуру ключ=значение
и соединяется в цепочку с помощью &
. Также важно знать, что ПП Next отдает тебе в виде объекта с ключами и значениями, поэтому его ПП имеет следующую форму:
const searchParams = {
tag: 'next',
difficulty: 'easy',
...
}
Напомню, что ПП может быть много, поэтому можно легко забыть, какие ПП страница должна использовать, а также легко допустить опечатку и приложение сломается (извечная проблема JavaScript, поздравляю, если ты все еще не перешел на TypeScript).
Для типизации ПП нужно также использовать дженерик, чтобы указать их набор для каждой отдельной страницы. Дженерик будет просить массив строк, каждая из которых представляет имя каждого из возможных ПП. А теперь иди дописывай тип TNextPage
новым дженериком.
export type TNextPage<P extends string = 'id', SP extends string[] = []> = {
params: Record<P, string>
searchParams: Record<SearchParams[number], string | null>
}
SP
ожидает массив строк, который изначально пуст. Очень удобно.searchParams
представляет собой объект, в котором ключом выступает каждое значение из объединения, а значением является строка или ее отстутсивие. Это возможно благодаря шаблонной записи SearchParams[number]
, которая подразумевает, что из массива ты вытаскиваешь значение по индексу, но не указываешь его конкретно. Это можно назвать шаблонным перебором списка или как-то так. Благодаря этому трюку можно получить типизированный объект searchParams
.Теперь ты можешь применить этот тип к какой-нибудь странице.
// app/blog/page.tsx
import { TNextPage } from '@/utils'
// Тип never, потому что страница не динамическая. Также нужно передать tag и difficulty в качестве ПП.
export type TBlogPage = TNextPage<never, ['tag', 'difficulty']>
export default function BlogPage({ params, searchParams }: TBlogPage) {
const id = params.id // Ошибка: нельзя обращаться к never.
const { tag, difficulty } = searchParams
// tag = "next"
// difficulty = "hard"
}
Специальный тип TNextPage
делает работу с params
и searchParams
немного проще, ты также видишь подсказки от VSCode и TypeScript, в качестве бонуса твой проект откажется собираться, если увидит опечатку в коде. В одной из следующих статей я расскажу тебе, как написать кастомный хук для удобного управления searchParams
в текущем URL. Этот хук будет очень простым, но от того не менее полезным, а также он будет использовать сегодняшний TNextPage
.