Dev Highlights

Доки по разработке

This project is maintained by teniryte

1. Основы App Router в Next.js

В этом файле — базовый «конспект по умолчанию» по структуре проектов на Next.js с App Router, опирающийся на ваш код в src/app.

1.1. Структура проекта и роль папки app

Принцип: в режиме App Router всё приложение строится вокруг папки app.
Каждая подпапка в app — это маршрут, а специальные файлы в этих папках определяют поведение страницы.

В вашем проекте есть:

Минимальный пример маршрута (аналог вашего src/app/page.tsx):

'use client';

export default function HomePage() {
  return <div>HOME</div>;
}

Здесь используется директива 'use client', поэтому компонент рендерится как клиентский (о различиях — ниже).

1.2. Root Layout: общий макет приложения

Файл src/app/layout.tsx определяет HTML-каркас всего приложения:

Ключевые идеи, которые важно запомнить:

Упрощённая структура root layout:

import type { Metadata } from 'next';
import { SomeFont } from 'next/font/google';
import '@/styles/globals.scss';

const someFont = SomeFont({
  variable: '--font-some-font',
  subsets: ['latin'],
});

export const metadata: Metadata = {
  title: 'Название приложения',
  description: 'Описание приложения',
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={someFont.variable}>
        {/* Здесь можно подключить любые глобальные компоненты */}
        {children}
      </body>
    </html>
  );
}

1.3. Клиентские и серверные компоненты

Next.js по умолчанию делает компоненты серверными.
Чтобы компонент стал клиентским, в начале файла нужно написать директиву:

'use client';

Серверные компоненты:

Клиентские компоненты:

Типичный пример клиентского компонента:

'use client';

import { useState, useEffect } from 'react';

export function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('Count changed', count);
  }, [count]);

  return (
    <button onClick={() => setCount((c) => c + 1)}>
      Count: {count}
    </button>
  );
}

Типичный пример серверного компонента:

// файл БЕЗ 'use client'
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

export default async function UsersPage() {
  const users = await prisma.user.findMany();

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.email}</li>
      ))}
    </ul>
  );
}

1.4. Маршрутизация на основе файловой системы

Принцип: путь к файлу page.tsx внутри app напрямую определяет URL.

Динамические сегменты:

Пример:

// app/blog/[slug]/page.tsx
export default async function BlogPostPage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;

  // Например, загрузка поста по slug
  const post = await fetch(`https://example.com/posts/${slug}`).then((res) =>
    res.json(),
  );

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

Вариант с синхронными params (чаще используемый в актуальных примерах):

export default async function BlogPostPage({
  params,
}: {
  params: { slug: string };
}) {
  const { slug } = params;
  // ...
}

1.5. Metadata и SEO на уровне маршрутов

Каждая страница/маршрут может определять свои метаданные через экспорт metadata или функцию generateMetadata.

Статические метаданные:

import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'Моя страница',
  description: 'Описание страницы',
};

export default function Page() {
  return <div>Контент</div>;
}

Динамические метаданные (пример — ISR-модуль с использованием параметра slug):

export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;

  return {
    title: `Пост ${slug}`,
  };
}

1.6. Работа с данными в серверных компонентах

Серверные компоненты — идеальное место для:

Пример: загрузка данных и передача их в дочерний компонент:

// Серверный компонент
import { PostsList } from './PostsList'; // допустим, это клиентский компонент

export default async function PostsPage() {
  const posts = await fetch('https://example.com/posts').then((res) =>
    res.json(),
  );

  return (
    <section>
      <h1>Posts</h1>
      <PostsList posts={posts} />
    </section>
  );
}
// Клиентский компонент
'use client';

export function PostsList({ posts }: { posts: any[] }) {
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

1.7. Подключение глобальных стилей и шрифтов

В RootLayout удобно подключать:

Паттерн подключения глобальных стилей:

// layout.tsx
import '@/styles/globals.scss';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

Паттерн подключения шрифтов:

import { Roboto } from 'next/font/google';

const roboto = Roboto({
  subsets: ['latin'],
  variable: '--font-roboto',
});

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body className={roboto.variable}>{children}</body>
    </html>
  );
}

1.8. Общие принципы организации кода

Подводя итог базовой структуре приложения:

Эти принципы — фундамент для следующих глав: формы и server actions, ISR/данные, lazy loading, MDX, SEO и т.д.