lenec ru

← все посты

OAuth 2.0 + PKCE: безопасная авторизация для SPA и мобильных приложений

11K

App Router в Next.js — это не просто новая файловая структура. Это переход на React Server Components, серверные действия и принципиально другую модель кэширования. Переход с Pages Router ломает привычки, но даёт мощные инструменты. Разберём ключевые фичи, паттерны и грабли, на которые наступают все.

App Router vs Pages Router: ключевые отличия

Pages Router — каждый файл в pages/ это маршрут. Data fetching через getServerSideProps/getStaticProps. Все компоненты — клиентские (гидрируются полностью).

App Router — файлы в app/. Компоненты по умолчанию серверные. Data fetching — прямо в компоненте через async/await. Layouts сохраняют состояние между навигациями. Streaming из коробки.

// app/users/page.tsx — Server Component по умолчанию
import { db } from '@/lib/db';

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

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

Нет getServerSideProps, нет отдельного слоя data fetching — запрос прямо в компоненте. Код не попадает в клиентский бандл.

Server Actions: формы без API routes

Server Actions — функции, которые выполняются на сервере, но вызываются из клиентских форм. Progressive enhancement: работают даже без JavaScript в браузере.

// app/actions.ts
"use server";

import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';

export async function createUser(formData: FormData) {
  const name = formData.get('name') as string;
  const email = formData.get('email') as string;

  await db.user.create({ data: { name, email } });
  revalidatePath('/users');
}
// app/users/new/page.tsx
import { createUser } from '@/app/actions';

export default function NewUserPage() {
  return (
    <form action={createUser}>
      <input name="name" placeholder="Имя" required />
      <input name="email" type="email" placeholder="Email" required />
      <button type="submit">Создать</button>
    </form>
  );
}

Не нужен API route, не нужен fetch на клиенте, не нужен useState для формы. Форма отправляется нативно, сервер обрабатывает, страница обновляется. Для интерактивности (loading state, оптимистичные обновления) — useFormStatus и useOptimistic.

Кэширование: fetch cache, revalidateTag, unstable_cache

Next.js агрессивно кэширует по умолчанию. Это главный источник путаницы:

// Кэшируется навсегда (по умолчанию в App Router)
const data = await fetch('https://api.example.com/posts');

// Ревалидация по времени
const data = await fetch('https://api.example.com/posts', {
  next: { revalidate: 60 }, // обновлять каждые 60 секунд
});

// Без кэша
const data = await fetch('https://api.example.com/posts', {
  cache: 'no-store',
});

// Ревалидация по тегу
const data = await fetch('https://api.example.com/posts', {
  next: { tags: ['posts'] },
});

// В Server Action после мутации:
revalidateTag('posts');

Для кэширования не-fetch данных (ORM, SDK) — unstable_cache:

import { unstable_cache } from 'next/cache';

const getCachedUsers = unstable_cache(
  async () => db.user.findMany(),
  ['users-list'],
  { revalidate: 300, tags: ['users'] }
);

Layouts и loading/error boundaries

App Router использует вложенные layouts, которые не перерендериваются при навигации между дочерними страницами:

// app/dashboard/layout.tsx
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="flex">
      <Sidebar />
      <main className="flex-1">{children}</main>
    </div>
  );
}

// app/dashboard/loading.tsx — автоматический Suspense boundary
export default function Loading() {
  return <Skeleton />;
}

// app/dashboard/error.tsx — автоматический Error boundary
"use client";
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
  return (
    <div>
      <p>Ошибка: {error.message}</p>
      <button onClick={reset}>Повторить</button>
    </div>
  );
}

Streaming работает автоматически: layout отправляется сразу, а loading.tsx показывается пока страница загружает данные.

Подводные камни

Кэш по умолчанию. В App Router fetch кэшируется навсегда, если не указать иное. Данные не обновляются — разработчик думает, что API сломан. Решение: явно указывайте cache: 'no-store' или revalidate.

cookies() и headers() делают страницу динамической. Вызов этих функций в Server Component отключает статическую генерацию. Страница рендерится на каждый запрос. Если нужна статика с персонализацией — выносите динамическую часть в Client Component.

Нельзя передать функции из Server в Client Component. Props между серверным и клиентским миром сериализуются в JSON. Функции, Date, Map — не пройдут.

Двойной рендер в dev-режиме. React Strict Mode рендерит компоненты дважды. Это не баг — это проверка на side effects. В продакшене рендер одинарный.

Когда App Router не нужен

  • SPA без SSR — если приложение за авторизацией и SEO не важен, Vite + React Router проще
  • Существующий проект на Pages Router — миграция трудоёмкая, а выигрыш не всегда оправдан
  • Простой статический сайт — Astro или Pages Router с getStaticProps проще
  • Команда не готова к RSC — ментальная модель сложнее, ошибки неочевидны

App Router — мощный инструмент для проектов, где важны SEO, скорость загрузки и серверный рендеринг. Но его сложность оправдана не всегда. Если ваш проект — дашборд за логином, классический SPA может быть лучшим выбором.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.