6. Получение данных
📡 Получение данных в Next.js
Заголовок раздела «📡 Получение данных в Next.js»Получение данных — сердце любого приложения. В Next.js App Router это кардинально проще и мощнее, чем в обычном React. Никаких useEffect(() => { fetch... }, []) — данные приходят там, где нужны! 🎯
Обычный React: Next.js (App Router):──────────────── ──────────────────────useEffect async функция-компонент→ setState(loading) → const data = await fetch(...)→ fetch('/api/data') → return <div>{data}</div>→ setState(data)→ render с данными Сервер отдаёт готовый HTML!→ клиент видит spinner→ клиент видит данные🚀 Базовый fetch в Server Components
Заголовок раздела «🚀 Базовый fetch в Server Components»export default async function PostsPage() { // fetch прямо в компоненте — без useEffect! const response = await fetch('https://jsonplaceholder.typicode.com/posts'); const posts = await response.json();
return ( <main> <h1>Посты ({posts.length})</h1> <ul> {posts.slice(0, 10).map((post: any) => ( <li key={post.id}> <strong>{post.title}</strong> <p>{post.body}</p> </li> ))} </ul> </main> );}Но это работает с любым источником данных:
// 1. Внешний APIconst data = await fetch('https://api.example.com/data').then(r => r.json());
// 2. Прямой запрос к базе данных (ORM)import { db } from '@/lib/prisma';const users = await db.user.findMany({ where: { active: true } });
// 3. Файловая системаimport { readFile } from 'fs/promises';const content = await readFile('./data/posts.json', 'utf-8');const posts = JSON.parse(content);
// 4. SDK/сторонние сервисыimport Stripe from 'stripe';const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);const products = await stripe.products.list({ limit: 10 });⚡ Параллельная загрузка данных
Заголовок раздела «⚡ Параллельная загрузка данных»Избегай водопадов! Запускай независимые запросы одновременно:
// ❌ ПЛОХО: Последовательные запросы (водопад)export default async function Dashboard() { const user = await getUser(); // 100ms const posts = await getPosts(); // 100ms const stats = await getStats(); // 100ms // Итого: 300ms последовательно!
return <div>...</div>;}
// ✅ ХОРОШО: Параллельные запросы с Promise.allexport default async function Dashboard() { const [user, posts, stats] = await Promise.all([ getUser(), // ┐ getPosts(), // ├─ Все три запроса параллельно! getStats(), // ┘ ]); // Итого: ~100ms (максимум из трёх)!
return ( <div> <UserCard user={user} /> <PostsList posts={posts} /> <StatsGrid stats={stats} /> </div> );}// Параллельные запросы с именованными переменными (читаемее)export default async function DashboardPage() { // Стартуем все запросы одновременно const userPromise = db.user.findUnique({ where: { id: userId } }); const postsPromise = db.post.findMany({ where: { authorId: userId }, take: 5 }); const statsPromise = getAnalytics(userId);
// Ждём все const [user, posts, stats] = await Promise.all([ userPromise, postsPromise, statsPromise, ]);
return <Dashboard user={user} posts={posts} stats={stats} />;}🔗 Последовательная загрузка (когда нужна)
Заголовок раздела «🔗 Последовательная загрузка (когда нужна)»Иногда данные зависят друг от друга:
// Последовательно — когда результат одного запроса нужен для следующегоexport default async function UserPostsPage({ params,}: { params: Promise<{ userId: string }>;}) { const { userId } = await params;
// Сначала получаем пользователя const user = await db.user.findUnique({ where: { id: userId } });
if (!user) notFound();
// Потом его посты (нужен user.teamId из первого запроса) const posts = await db.post.findMany({ where: { authorId: user.id, teamId: user.teamId, // Зависит от первого запроса! }, });
return ( <div> <h1>Посты {user.name}</h1> <PostsList posts={posts} /> </div> );}🗃️ Кеширование fetch()
Заголовок раздела «🗃️ Кеширование fetch()»Next.js расширяет встроенный fetch с поддержкой кеширования:
// 1. Кешировать навсегда (ISR / Static)// Данные запрашиваются один раз при сборке, потом из кешаconst data = await fetch('https://api.example.com/static-data', { cache: 'force-cache', // По умолчанию в Next.js 14 (изменилось в 15!)});
// 2. Никогда не кешировать (SSR)// Свежие данные при каждом запросеconst data = await fetch('https://api.example.com/live-data', { cache: 'no-store',});
// 3. Кешировать с периодическим обновлением (ISR)const data = await fetch('https://api.example.com/data', { next: { revalidate: 3600 }, // Обновлять каждый час});
// 4. Кешировать с тегами (on-demand revalidation)const posts = await fetch('https://api.example.com/posts', { next: { tags: ['posts'] }, // Можно сбросить тег программно!});Важно для Next.js 15: кеширование изменилось!
// Next.js 14 — кешировалось по умолчаниюconst data = await fetch('/api/data'); // → force-cache
// Next.js 15 — НЕ кешируется по умолчанию!const data = await fetch('/api/data'); // → no-store
// Если нужен кеш — указывай явно:const data = await fetch('/api/data', { cache: 'force-cache' });// илиconst data = await fetch('/api/data', { next: { revalidate: 60 } });🔁 Ревалидация: Обновление кешированных данных
Заголовок раздела «🔁 Ревалидация: Обновление кешированных данных»// Метод 1: Временная ревалидацияexport const revalidate = 3600; // Страница обновляется каждый час
export default async function BlogPage() { const posts = await fetch('https://api.blog.com/posts').then(r => r.json()); return <PostsList posts={posts} />;}
// Метод 2: Теговая ревалидация (on-demand)// app/api/revalidate/route.tsimport { revalidateTag } from 'next/cache';import { NextRequest } from 'next/server';
export async function POST(request: NextRequest) { const { tag, secret } = await request.json();
// Проверяем секрет (безопасность!) if (secret !== process.env.REVALIDATION_SECRET) { return Response.json({ error: 'Unauthorized' }, { status: 401 }); }
revalidateTag(tag); // Сбросить кеш для этого тега return Response.json({ revalidated: true, tag });}
// Webhook от CMS: когда обновляется контент → POST /api/revalidate { tag: 'posts' }🎭 Suspense + Streaming
Заголовок раздела «🎭 Suspense + Streaming»Streaming позволяет отправлять HTML по частям, не дожидаясь всех данных:
import { Suspense } from 'react';
export default function DashboardPage() { return ( <div> <h1>Дашборд</h1>
{/* Быстрые данные — рендерятся сразу */} <UserGreeting />
{/* Медленные данные — не блокируют страницу */} <Suspense fallback={<StatsSkeleton />}> <SlowStats /> {/* Загружается параллельно! */} </Suspense>
<Suspense fallback={<ChartSkeleton />}> <RevenueChart /> {/* Загружается параллельно! */} </Suspense>
<Suspense fallback={<TableSkeleton />}> <TransactionsTable /> {/* Загружается параллельно! */} </Suspense> </div> );}
// Каждый компонент загружает свои данные независимо!async function SlowStats() { const stats = await getStats(); // Медленный запрос return <StatsGrid stats={stats} />;}
async function RevenueChart() { const revenue = await getRevenue(); // Другой медленный запрос return <Chart data={revenue} />;}Без Streaming:[===========================] 3s → Страница готова
С Streaming:[=====] 0.5s → Базовый HTML[=========] 1s → SlowStats готов[==============] 1.5s → RevenueChart готов[====================] 2s → TransactionsTable готов🔒 unstable_cache: Кеширование не-fetch запросов
Заголовок раздела «🔒 unstable_cache: Кеширование не-fetch запросов»Для ORM и прямых запросов к БД:
import { unstable_cache } from 'next/cache';
// Кешируем функцию с тегамиconst getCachedPosts = unstable_cache( async (userId: string) => { return await db.post.findMany({ where: { authorId: userId }, include: { author: true }, }); }, ['user-posts'], // ключ кеша { revalidate: 3600, // обновлять каждый час tags: ['posts'], // тег для on-demand сброса });
// Использованиеexport default async function UserPostsPage({ params }) { const { userId } = await params; const posts = await getCachedPosts(userId); // Кешируется! return <PostsList posts={posts} />;}📝 useFormState и Server Actions (бонус)
Заголовок раздела «📝 useFormState и Server Actions (бонус)»Формы с данными с сервера:
'use server';
export async function searchPosts(prevState: any, formData: FormData) { const query = formData.get('query') as string;
if (!query || query.length < 2) { return { error: 'Введите минимум 2 символа', posts: [] }; }
const posts = await db.post.findMany({ where: { OR: [ { title: { contains: query, mode: 'insensitive' } }, { content: { contains: query, mode: 'insensitive' } }, ], }, take: 10, });
return { posts, query, error: null };}
// app/search/page.tsx'use client';import { useActionState } from 'react'; // React 19import { searchPosts } from '../actions';
export default function SearchPage() { const [state, formAction, isPending] = useActionState(searchPosts, { posts: [], error: null, query: '', });
return ( <div> <form action={formAction}> <input name="query" placeholder="Поиск..." /> <button type="submit" disabled={isPending}> {isPending ? 'Ищем...' : 'Найти'} </button> </form>
{state.error && <p style={{ color: 'red' }}>{state.error}</p>}
{state.posts.map(post => ( <div key={post.id}>{post.title}</div> ))} </div> );}🎯 Резюме урока
Заголовок раздела «🎯 Резюме урока»- fetch() в Server Components — async/await прямо в компоненте
- Promise.all() — параллельные запросы, избегай водопадов
- Кеширование —
cache: 'force-cache'/next: { revalidate: N } - Теговая ревалидация — on-demand сброс кеша
- Suspense + Streaming — не жди медленных данных, стримь HTML
- unstable_cache — кеш для ORM и не-fetch запросов