JavaRush /Курсы /Модуль 4: Node.js, Next.js и Angular /Вопросы и ответы по маршрутизации App Router

Вопросы и ответы по маршрутизации App Router

Модуль 4: Node.js, Next.js и Angular
8 уровень , 4 лекция
Открыта

1. "Я создал файл, а страница не отображается! Где мой контент?!"

Это классика жанра! Вы создали app/products/page.tsx, ожидаете увидеть что-то по адресу /products, а в браузере — ошибка 404 или вообще ничего.

В чем проблема?
Самая распространенная причина — отсутствие или неправильное имя файла page.tsx (или page.js, page.jsx, page.ts). Next.js строго придерживается соглашения по именованию. Если вы назвали файл index.tsx, main.tsx, или просто Products.tsx, Next.js не будет считать его страницей для данного маршрута. Только page сработает.

Пример (неправильно):


app/
├── products/
│   └── ProductsPage.tsx  // ❌ Не сработает!

Как исправить?
Переименуйте файл в page.tsx (или соответствующее расширение).

Пример (правильно):


// app/products/page.tsx
// Этот файл будет отображаться по адресу /products

export default function ProductsPage() {
  return (
    <div>
      <h1>Список товаров на складе</h1>
      <p>Здесь будут отображены все наши товары.</p>
    </div>
  );
}

Ваш учебный проект: Допустим, мы хотим страницу /dashboard, где будет сводка по складу. Для этого создаем app/dashboard/page.tsx.


// app/dashboard/page.tsx
export default function DashboardPage() {
  return (
    <main>
      <h2>Добро пожаловать на панель управления складом!</h2>
      <p>Здесь вы можете отслеживать статистику и управлять инвентарем.</p>
    </main>
  );
}

Теперь при переходе на /dashboard мы увидим содержимое DashboardPage.

2. "Как мне сделать главную страницу моего сайта? app/index.tsx не работает!"

Еще одна классика, особенно для тех, кто привык к index.html или index.js в других фреймворках.

В чем проблема?
Для корневой страницы вашего приложения (/), Next.js также ожидает файл page.tsx (или аналог), но расположенный прямо в папке app/. То есть, app/page.tsx.

Как исправить?
Создайте файл page.tsx непосредственно в корневой папке app/.

Пример:


// app/page.tsx
// Это будет ваша главная страница, доступная по адресу "/"

export default function HomePage() {
  return (
    <div>
      <h1>Добро пожаловать в Систему Управления Складом!</h1>
      <p>Начните работу с нашими продуктами или перейдите в панель управления.</p>
    </div>
  );
}

3. "Я использую динамический маршрут [id], но req.params пустой! Где мои параметры?"

Если вы пришли из мира Express.js или даже Page Router (где API Routes использовали req.query или req.params), то можете столкнуться с этой проблемой.

В чем проблема?
В App Router, для компонентов страниц (page.tsx) параметры маршрута (такие как id в [id]) передаются не через объект req (который используется в Route Handlers), а напрямую как пропсы в ваш компонент страницы.

Как исправить?
Ваш компонент page.tsx должен принимать пропсы. В этих пропсах будет объект params, который содержит значения динамических сегментов.

Пример:
Допустим, у нас есть страница детали товара: app/products/[productId]/page.tsx.


// app/products/[productId]/page.tsx
interface ProductPageProps {
  params: {
    productId: string; // Имя должно совпадать с именем папки в скобках
  };
}

export default function ProductDetailPage({ params }: ProductPageProps) {
  const { productId } = params; // Вот они, наши параметры!

  return (
    <div>
      <h1>Детали товара: {productId}</h1>
      <p>Здесь будет отображаться подробная информация о товаре с ID: {productId}.</p>
      {/* Здесь можно fetch'ить данные о товаре по productId */}
    </div>
  );
}

Теперь, если пользователь перейдет на /products/123, productId будет равен '123'.

4. "Мой [...slug] не работает для базового URL!"

Вы создали app/blog/[...slug]/page.tsx, ожидаете, что /blog и /blog/my-post будут работать, но /blog выдает 404.

В чем проблема?
[...slug] (catch-all segment) требует, чтобы после основной части маршрута был хотя бы один дополнительный сегмент. Это значит, что /blog/ сам по себе не будет маршрутизироваться к [...slug]. Ему нужен /blog/что-то.

Как исправить?
Если вы хотите, чтобы /blog (базовый URL) тоже работал, вам нужно создать отдельный файл page.tsx непосредственно в папке blog:


app/
├── blog/
│   ├── page.tsx          // 👈 Для маршрута /blog
│   └── [...slug]/
│       └── page.tsx      // 👈 Для маршрутов /blog/post, /blog/category/post и т.д.

Содержимое app/blog/page.tsx:


// app/blog/page.tsx
export default function BlogIndexPage() {
  return (
    <div>
      <h1>Главная страница блога</h1>
      <p>Здесь список всех наших статей.</p>
    </div>
  );
}

Содержимое app/blog/[...slug]/page.tsx:


// app/blog/[...slug]/page.tsx
interface BlogPostPageProps {
  params: {
    slug: string[]; // Массив строк для catch-all сегментов
  };
}

export default function BlogPostPage({ params }: BlogPostPageProps) {
  const fullPath = params.slug.join('/'); // Собираем путь из массива

  return (
    <div>
      <h1>Статья по пути: /{fullPath}</h1>
      <p>Здесь содержимое статьи.</p>
    </div>
  );
}

Теперь /blog покажет BlogIndexPage, а /blog/hello-world покажет BlogPostPage с params.slug = ['hello-world'].

5. "Когда использовать [...slug], а когда [[...slug]]?"

Это очень частый вопрос! Зачем нужны двойные скобки, если есть обычные?

Отличие:

  • [...slug] (одинарные скобки) — обязательный catch-all. Требует минимум один сегмент после базового пути. Если у вас app/posts/[...path]/page.tsx, то /posts выдаст 404, а /posts/first-post или /posts/2023/january/post-title сработают.
  • [[...slug]] (двойные скобки) — опциональный catch-all. Работает как для базового пути, так и для всех последующих сегментов. Если у вас app/pages/[[...path]]/page.tsx, то /pages будет работать (и path будет пустым массивом), а также /pages/sub-page и /pages/nested/path.

Пример (из нашей документации по складу):
Допустим, у нас есть раздел "Документация".

  • app/docs/[...path]/page.tsx:
    • /docs ➡️ 404
    • /docs/getting-started ➡️ params.path = ['getting-started']
    • /docs/api/usage ➡️ params.path = ['api', 'usage']
  • app/docs/[[...path]]/page.tsx:
    • /docs ➡️ params.path = [] (пустой массив, но страница отобразится!)
    • /docs/getting-started ➡️ params.path = ['getting-started']
    • /docs/api/usage ➡️ params.path = ['api', 'usage']

Когда что использовать?

  • Используйте [...slug], если базовый URL должен быть отдельной страницей или если вы всегда ожидаете дополнительные сегменты. Например, для блога, где /blog — это список статей (свой page.tsx), а /blog/статья — конкретная статья.
  • Используйте [[...slug]], если одна и та же страница может обслуживать как базовый URL, так и его подпути, и вам не нужна отдельная корневая страница для этого маршрута. Например, для документации, где /docs показывает введение, а /docs/chapter1 — конкретную главу, и всё это обрабатывается одним компонентом.

6. "У меня есть app/item/new/page.tsx и app/item/[id]/page.tsx. Когда я иду на /item/new, иногда показывает страницу [id]!"

Ох уж эта неочевидная магия! Next.js имеет свой порядок разрешения маршрутов.

В чем проблема?
Next.js маршрутизатор приоритезирует статические маршруты над динамическими, а динамические над catch-all. Однако, если у вас есть статический сегмент (new) и динамический ([id]) на одном уровне, Next.js обычно отдаст предпочтение точному совпадению статического маршрута.
Проблема чаще возникает, если папка new не содержит page.tsx или есть какие-то другие пересечения. Например, если внутри app/item/new нет page.tsx, то /item/new может быть пойман [id] как id='new'.

Как избежать?
Убедитесь, что для каждого статического маршрута, который может конфликтовать с динамическим, у вас есть своя папка с page.tsx внутри.

Используйте отдельные папки для статических маршрутов:


app/
├── item/
│   ├── [id]/
│   │   └── page.tsx  // Страница деталей товара, например /item/123
│   └── new/
│       └── page.tsx  // Страница для создания нового товара, /item/new

В этом случае /item/new будет корректно обработан app/item/new/page.tsx, потому что это точное статическое совпадение. А /item/123 будет обработан app/item/[id]/page.tsx.

Это одно из самых важных правил: статические маршруты имеют приоритет над динамическими. Всегда создавайте точные статические папки/файлы, если у вас есть пересечения с динамическими.

Ваш интернет-магазин :

  • /products/[productId]/page.tsx (для просмотра и редактирования товара по ID)
  • /products/create/page.tsx (для создания нового товара)

Next.js прекрасно поймет, что при запросе /products/create нужно отдать страницу из папки create, а при запросе /products/456 — страницу из [productId], где productId будет равен 456.

7. "Я передаю параметры в дочерний компонент, но он не обновляется, когда меняется URL!"

Это заблуждение возникает, когда смешивают Server Components и Client Components.

В чем проблема?
По умолчанию, все компоненты в App Router — это Server Components. Они рендерятся на сервере (один раз при запросе, если не отключено кеширование, о чем будет позже). Если вы передали params (или любые другие пропсы) из Server Component в Client Component (тот, что с 'use client'), то этот Client Component будет использовать те пропсы, которые были переданы ему на сервере. При клиентской навигации (например, через <Link>) Server Components не перерендериваются без специальных инструкций, а значит, и пропсы могут не обновиться, если вы полагаетесь на механизм, который работает только на сервере.

Как исправить?

  • Правильное использование 'use client': Убедитесь, что ваш page.tsx (который по умолчанию Server Component) получает params и передает их явно в ваш Client Component. Client Component должен использовать эти пропсы.
  • Для Client Components, которым нужны параметры URL: Используйте хук useParams из next/navigation. Этот хук доступен только в Client Components.

Пример:
app/items/[itemId]/page.tsx (Server Component)


// app/items/[itemId]/page.tsx
import ClientItemDetails from './ClientItemDetails'; // Импортируем Client Component

interface ItemPageProps {
  params: {
    itemId: string;
  };
}

export default function ItemDetailPage({ params }: ItemPageProps) {
  // params.itemId доступен здесь, в Server Component
  const { itemId } = params;

  return (
    <div>
      <h1>Информация о товаре (Server Component)</h1>
      <ClientItemDetails itemId={itemId} /> {/* Передаем itemId как пропс */}
    </div>
  );
}

app/items/[itemId]/ClientItemDetails.tsx (Client Component)


// app/items/[itemId]/ClientItemDetails.tsx
'use client'; // ОБЯЗАТЕЛЬНО!

import { useParams } from 'next/navigation'; // Импортируем useParams для Client Components
import { useEffect, useState } from 'react';

interface ClientItemDetailsProps {
  itemId: string; // Принимаем itemId как пропс
}

export default function ClientItemDetails({ itemId }: ClientItemDetailsProps) {
  // А здесь, внутри Client Component, мы можем получить params
  // Но гораздо надежнее использовать пропсы, переданные Server Component
  // Или, если компонент standalone, useParams
  const clientParams = useParams();
  const [data, setData] = useState<string | null>(null);

  useEffect(() => {
    // Здесь мы используем пропс, который пришел от Server Component
    // или clientParams.itemId, если компонент не был вложен.
    // Если компонент является прямым потомком страницы и ему нужен params,
    // оба варианта (пропс или useParams) будут работать,
    // но useParams подходит, если компоненту НЕ передают params явно.
    setData(`Загрузка данных для товара: ${itemId} (получено из пропсов)`);
    // Или так: setData(`Загрузка данных для товара: ${clientParams.itemId} (получено из useParams)`);
  }, [itemId, clientParams.itemId]); // Зависимости для useEffect

  return (
    <div style={{ border: '1px solid blue', padding: '10px', marginTop: '10px' }}>
      <h2>Client Component:</h2>
      <p>{data}</p>
      <p>ID товара (из пропсов): {itemId}</p>
      <p>ID товара (из useParams, если нужно): {clientParams.itemId}</p>
      {/* Здесь может быть интерактивная логика */}
    </div>
  );
}

Ключевой момент: Если Client Component является дочерним для Server Component, то Server Component должен явно передавать ему нужные пропсы. Если Client Component сам является страницей (т.е. page.tsx помечен 'use client'), или же он находится в иерархии, где вышестоящие Server Components не передают нужные данные, то useParams в Client Component — ваш друг. Но помните, что useParams работает только на клиенте!

8. "Что будет, если я создам app/products/page.tsx и app/[slug]/products/page.tsx?"

Такой вопрос демонстрирует глубокое понимание файловой маршрутизации.

В чем проблема?
Это пример так называемого "конфликта маршрутов" или неоднозначности. Next.js не сможет однозначно определить, какой маршрут вы имели в виду для /products.

Как Next.js это обрабатывает?
В общем случае, Next.js стремится к максимальной специфичности. Он будет сначала искать точные статические совпадения, затем динамические сегменты, а затем catch-all/optional catch-all. В данном случае, app/products/page.tsx будет иметь приоритет над app/[slug]/products/page.tsx для маршрута /products.
Если вы попытаетесь создать app/products/page.tsx и app/[something]/page.tsx, то /products всегда будет отображать app/products/page.tsx, а /about будет отображать app/[something]/page.tsx с something='about'.

Совет: Избегайте таких пересекающихся маршрутов, где динамический сегмент может перехватить статический на том же уровне иерархии, если это не явно задумано. Next.js достаточно умён, но четкая иерархия папок всегда лучше.

3
Опрос
File-based routing, 8 уровень, 4 лекция
Недоступен
File-based routing
File-based routing
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ