OAuth 2.0 + PKCE: безопасная авторизация для SPA и мобильных приложений
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 может быть лучшим выбором.