В этой лекции мы рассмотрим фронтенд аспекты Next.js: файловую структуру для маршрутизации, динамические сегменты, серверные и клиентские компоненты, навигацию и хуки маршрутизации.
В Next.js используется файловая система для маршрутизации (file-based routing). Это означает, что структура папок автоматически определяет маршруты вашего приложения. Каждый файл в папке app становится маршрутом.
Основная структура проекта:
my-next-app/
├── app/
│ ├── page.tsx // / (главная страница)
│ ├── about/
│ │ └── page.tsx // /about
│ ├── blog/
│ │ └── page.tsx // /blog
│ └── layout.tsx // Общий layout
├── public/ // Статические файлы
├── package.json
└── next.config.js
Создание маршрутов через файлы:
page.tsx в корне app/:// app/page.tsx
// Этот компонент обслуживает маршрут /
export default function Home() {
return <h1>Добро пожаловать на главную</h1>;
}
Этот компонент доступен по адресу http://localhost:3000/
page.tsx внутри:// app/about/page.tsx
// Этот компонент обслуживает маршрут /about
export default function About() {
return <h1>О нас</h1>;
}
Этот компонент доступен по адресу http://localhost:3000/about
// app/blog/posts/page.tsx
// Этот компонент обслуживает маршрут /blog/posts
export default function BlogPosts() {
return <h1>Все посты блога</h1>;
}
Доступно по адресу http://localhost:3000/blog/posts
Преимущества файловой маршрутизации:
Динамические сегменты позволяют создавать маршруты с переменной частью в URL. Они обозначаются квадратными скобками [paramName].
Создание динамического маршрута:
Если вам нужна страница для отдельного поста с адресом вроде /blog/1 или /blog/my-first-post, используйте динамический сегмент:
app/
├── blog/
│ ├── page.tsx // /blog - список постов
│ └── [id]/
│ └── page.tsx // /blog/[id] - отдельный пост
ВАЖНО: В Next.js 13+ параметры должны быть получены асинхронно
Начиная с App Router, параметры маршрута передаются как Promise. Вы должны использовать await при работе с параметрами:
// app/blog/[id]/page.tsx
import React from 'react';
// Определяем тип пропса с параметрами
type BlogPostProps = {
params: Promise<{ // тип для асинхроного свойства
id: string; // id будет доступен из URL
}>;
}
// Компонент должен быть async чтобы использовать await
export default async function BlogPost({ params }: BlogPostProps) {
// ПРАВИЛЬНО: асинхронно получаем параметры из Promise
const { id } = await params;
return (
<div>
<h1>Пост с ID: {id}</h1>
<p>Здесь отобразится содержимое поста {id}</p>
</div>
);
}
Неправильно (старый способ):
// ❌ Так делать НЕЛЬЗЯ - параметры теперь Promise, не объект
export default function BlogPost({ params }: { params: { id: string } }) {
const { id } = params; // Ошибка! params - это Promise, нужен await
}
Множественные динамические сегменты:
app/
├── posts/
│ └── [year]/
│ └── [month]/
│ └── page.tsx
// app/posts/[year]/[month]/page.tsx
type PostsProps = {
params: Promise<{
year: string; // Год из URL
month: string; // Месяц из URL
}>;
}
export default async function Posts({ params }: PostsProps) {
// Распаковываем оба параметра из Promise
const { year, month } = await params;
return (
<div>
<h1>Посты за {month}/{year}</h1>
</div>
);
}
При переходе на /posts/2024/11 будут доступны параметры: year = "2024" и month = "11".
Link — встроенный компонент Next.js для оптимизированной навигации между страницами.
// app/page.tsx
import Link from 'next/link'; // Импортируем Link из next/link
export default function Home() {
return (
<div>
{/* Link используется вместо обычного <a> тега */}
<Link href="/about">О нас</Link>
<Link href="/blog">Блог</Link>
</div>
);
}
Динамические ссылки с параметрами:
// app/blog/page.tsx
'use client';
import Link from 'next/link';
type Post = {
id: number;
title: string;
}
export default function BlogPage() {
// Массив постов для примера
const posts: Post[] = [
{ id: 1, title: 'Моя первая статья' },
{ id: 2, title: 'Вторая статья' },
{ id: 3, title: 'Третья статья' }
];
return (
<div>
<h1>Блог</h1>
<ul>
{/* Для каждого поста создаем динамическую ссылку */}
{posts.map(post => (
<li key={post.id}>
{/* href строится динамически с ID поста */}
<Link href={`/blog/${post.id}`}>
{post.title}
</Link>
</li>
))}
</ul>
</div>
);
}
Преимущества Link:
В Next.js два типа компонентов:
| Тип | По умолчанию | Использование |
|---|---|---|
| Server | Да | Получение данных, БД, API ключи |
| Client | Нет (требует 'use client') |
Хуки, события, интерактивность |
Server Component (по умолчанию):
// app/blog/page.tsx - это Server Component по умолчанию (не нужен 'use client')
async function getBlogPosts() {
// Можем безопасно получать данные прямо в компоненте
const response = await fetch('http://localhost:3000/api/posts');
return response.json();
}
export default async function BlogPage() {
// Ждем получения постов
const posts = await getBlogPosts();
return (
<div>
<h1>Блог</h1>
<ul>
{/* Отображаем полученные посты */}
{posts.map((post: any) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
Client Component (требует 'use client'):
// app/components/Counter.tsx
'use client'; // ← Обязательно в начале файла! Говорит Next.js что это Client Component
import React, { useState } from 'react';
export default function Counter() {
// useState работает только в Client Components
const [count, setCount] = useState(0);
return (
<div>
<h2>Счетчик: {count}</h2>
{/* onClick событие работает только в Client Components */}
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
Когда писать 'use client':
✅ Пишите 'use client' если:
useState или useEffect — состояние и эффектыonClick, onChange, onSubmit — интерактивностьlocalStorage, window, document — браузерные API// Пример: форма с состоянием (НУЖЕН 'use client')
'use client'; // ← Требуется для этого компонента
import { useState } from 'react';
export default function UserForm() {
// Используем useState - нужен 'use client'
const [name, setName] = useState('');
const handleSubmit = (e: React.FormEvent) => {
// Слушаем событие onSubmit - нужен 'use client'
e.preventDefault();
// Отправляем данные на сервер через API
fetch('/api/users', {
method: 'POST',
body: JSON.stringify({ name })
});
};
return (
<form onSubmit={handleSubmit}>
{/* onChange событие - нужен 'use client' */}
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Введите имя"
/>
<button type="submit">Отправить</button>
</form>
);
}
❌ Не пишите 'use client' если:
async) — Server Component справитсяlocalStorage — это браузерный API для сохранения данных на клиентской стороне. Данные сохраняются локально на компьютере пользователя и сохраняются между сеансами браузера.
ВАЖНО: localStorage работает только в Client Components, так как это браузерный API.
Основные методы localStorage:
localStorage.setItem(key, value) — сохранить значениеlocalStorage.getItem(key) — получить значениеlocalStorage.removeItem(key) — удалить значениеlocalStorage.clear() — очистить все значенияПример — счетчик посещений:
// app/components/VisitCounter.tsx
'use client'; // ← Требуется для localStorage
import React, { useState, useEffect } from 'react';
export default function VisitCounter() {
const [visits, setVisits] = useState(0);
const [isMounted, setIsMounted] = useState(false); // Флаг для проверки монтирования
useEffect(() => {
// Помечаем что компонент смонтирован
setIsMounted(true);
// Получаем количество посещений из localStorage
const saved = localStorage.getItem('visits');
const count = saved ? parseInt(saved) : 0; // Если нет - начинаем с 0
const newCount = count + 1; // Увеличиваем счетчик
// Сохраняем новое значение в localStorage
localStorage.setItem('visits', newCount.toString());
setVisits(newCount);
}, []); // Эффект запускается только один раз при монтировании
// Не рендерим ничего пока компонент не смонтирован (избегаем ошибок)
if (!isMounted) return null;
return <p>Вы посетили эту страницу {visits} раз</p>;
}
Пример — сохранение значения поиска:
// app/components/SearchInput.tsx
'use client'; // ← Требуется для localStorage и onChange события
import React, { useState, useEffect } from 'react';
export default function SearchInput() {
const [query, setQuery] = useState(''); // Текущее значение поиска
const [isMounted, setIsMounted] = useState(false); // Флаг монтирования
useEffect(() => {
setIsMounted(true);
// При загрузке восстанавливаем последний поисковый запрос
const saved = localStorage.getItem('searchQuery');
if (saved) setQuery(saved);
}, []);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setQuery(value); // Обновляем состояние
localStorage.setItem('searchQuery', value); // Сохраняем в localStorage
};
if (!isMounted) return null; // Ждем монтирования
return (
<input
type="text"
value={query}
onChange={handleChange}
placeholder="Поиск..."
/>
);
}
ВАЖНЫЕ ЗАМЕЧАНИЯ ПО localStorage:
useEffect с проверкой isMounted), прежде чем обращаться к localStorageJSON.stringify() и JSON.parse() для объектовuseRouter — это хук Next.js для навигации в коде (программная навигация). Он используется в Client Components.
Синтаксис useRouter:
'use client'; // ← Требуется для useRouter
import { useRouter } from 'next/navigation'; // ← Обратите внимание: импортируем из 'next/navigation' (не 'next/router'!)
export default function MyComponent() {
// Получаем объект router для управления навигацией
const router = useRouter();
const handleClick = () => {
// Переходим на страницу /blog
router.push('/blog');
};
return <button onClick={handleClick}>Перейти в блог</button>;
}
Основные методы useRouter:
router.push(url) — переход на новую страницу:const handleNavigate = () => {
// Переходим на страницу /about
router.push('/about');
};
router.back() — переход на предыдущую страницу (как кнопка "назад" в браузере):const handleBack = () => {
// Возвращаемся на предыдущую страницу из истории
router.back();
};
router.forward() — переход на следующую страницу (как кнопка "вперед"):const handleForward = () => {
// Переходим на следующую страницу из истории
router.forward();
};
router.refresh() — обновление текущей страницы:const handleRefresh = () => {
// Обновляем текущую страницу
router.refresh();
};
Создайте сайт, который показывает актуальную погоду в Омске. Получайте погодные данные с внешнего сервиса (API), отображайте температуру и другие параметры. Дайте пользователю возможность выбрать период прогноза — завтра, неделя или две недели. Оформите интерфейс так, чтобы все основные показатели были сразу видны и легко сравнивались между разными днями.
Сделайте блог, где можно публиковать свои посты и просматривать чужие. Для хранения постов и хештегов используйте Supabase. На главной — лента с постами: в каждом отображайте заголовок, автора и хештеги. Реализуйте добавление новых постов через простую форму. Добавьте фильтрацию постов по хештегу — попробуйте придумать, как это лучше сделать.
Создайте библиотеку документов и книг. Сохраняйте данные о книгах, авторах и ссылках на PDF в Supabase. На сайте покажите каталог всех книг, реализуйте поиск или простую фильтрацию (по названию, автору, теме). Обеспечьте удобный просмотр PDF прямо в приложении — придумайте, на какой странице и как лучше это расположить.