Якщо ви коли-небудь відвідували Unsplash або десктопну версію Instagram, ви могли помітити, що в залежності від контексту вашого візиту ви отримаєте різний досвід користувача при збільшенні фотографії. Спробуйте зараз…
У вашому десктопному браузері перейдіть на Unsplash і знайдіть вашу улюблену тему (я шукаю фото про підводне плавання) — зверніть увагу на URL.
Пошук на unsplash.com за запитом "scuba", URL: https://unsplash.com/s/photos/scuba.
Клацнувши на фотографію в результатах пошуку, ви побачите діалогове вікно, що з’явиться поверх сторінки. Ви помітите, що URL зміниться на індивідуальний для обраного зображення:
- Приклад URL пошуку: https://unsplash.com/s/photos/scuba.
- Приклад URL фотографії: https://unsplash.com/photos/a-person-in-a-scuba-suit-is-standing-on-a-diving-device-3IhLFF6gvCs.
Перегляд окремої фотографії в модальному вікні, зверніть увагу на URL: https://unsplash.com/photos/a-person-in-a-scuba-suit-is-standing-on-a-diving-device-3IhLFF6gvCs.
Якщо ви оновите сторінку, поки діалогове вікно відкрите, логічно очікувати, що ви отримаєте такий самий досвід — результати пошуку на фоні та модальне вікно з великим зображенням.
Натомість, вебсайт відображає сторінку, яка повністю присвячена обраній вами фотографії, без результатів пошуку на фоні.
Коли модальне вікно оновлюється, URL залишається тим самим, але досвід змінюється.
Оскільки веб-сторінка не має контексту про історію пошуку, їй потрібно відобразити контент, специфічний для URL, що дає два можливих користувацьких досвіди:
- Коли застосунок має контекст наміру користувача — досвід користувача адаптований до контексту користувача. У цьому прикладі користувач шукає зображення і переглядає велике зображення в модальному вікні, при цьому результати пошуку залишаються доступними, але на фоні.
- Коли застосунок не має контексту наміру користувача — досвід користувача специфічний для глибокого посилання URL.
Цей підхід має кілька переваг:
- Це краще для SEO — кожну сторінку зображення можна індексувати пошуковою системою.
- Це зручніше для поділу — ресурс можна поділитися через глибоке посилання.
- Це легше для керування станом наміру користувача — збереження контексту користувача (їх результатів пошуку) на фоні.
- Це кращий досвід для користувача — модель закривається, коли ви повертаєтесь назад в історії (замість того щоб йти на попередній маршрут) і знову відкривається при переході вперед.
У Next.js цей процес можна реалізувати за допомогою деякої функціональності, введеної в версії 13. Перед початком цього навчального посібника ознайомтесь з документацією на сайті Next.js з наступних тем:
- Паралельні маршрути — для вставлення модального вікна на сторінку як слота.
- Перехоплення маршрутів — для роботи з API історії браузера.
- Динамічні маршрути — для показу окремих фотографій.
- Групи маршрутів — для ізоляції модальних вікон на окремих сторінках, щоб вони не конфліктували.
Мета
Цей посібник продемонструє базову реалізацію вебсайту Unsplash, використовуючи деякі з його зображень.
Користувач:
- Потрапить на сторінку, яка містить список зображень з Unsplash.
- Клацнувши на зображення, відкриється діалогове вікно як модальне, щоб показати зображення у великому форматі.
- Оновлення сторінки або поділення URL призведе до відображення окремої фотографії на сторінці без модального діалогового вікна.
Цей приклад використовує Material UI для швидкої розробки фронтенду; однак, ви можете застосувати ці концепції до вашої власної системи дизайну.
Щоб почати, ознайомтесь з кінцевою реалізацією коду на GitHub.
Розуміння основних файлів
Ось огляд найбільш важливих файлів, які є в додатку з репозиторію GitHub.
Спрощені фрагменти коду
Ми розглянемо кожен з файлів, але фрагменти коду спрощені для стислості. Повний код доступний в репозиторії на GitHub.
src/app/(home)/page.tsx — на GitHub
Ця сторінка містить список усіх фотографій з нашого масиву photos
, кожна з яких є посиланням на свою окрему сторінку для перегляду в великому форматі. Не має значення, чи URL відображається як діалогове вікно або окрема сторінка, Next.js подбає про це за нас.
import Link from "next/link";
{photos.map((photo) => (
))}
Пам'ятайте:
Для використання перехоплювачів маршрутів (route interceptors) потрібно використовувати компонентLink
з Next.js замість тега `` anchor.
Після перевірки репозиторію та запуску команд npm install
і npm run dev
, сторінка має виглядати наступним чином:
Перехід за адресою http://localhost:3000/ має показати список зображень з масиву photos.
src/app/(home)/layout.tsx — на GitHub
Слоти вбудовуються в макети (layouts) і не можуть бути вбудовані в саму сторінку. У нашому випадку, наш слот називається modals
.
import type { ReactNode } from "react";
export type HomeLayoutProps = Readonly>;
export default async function HomeLayout({
children,
modals,
}: HomeLayoutProps) {
return (
<>
{children}
{modals}
);
}
Ви помітите, що ці два файли (page.tsx
та layout.tsx
) знаходяться в папці з назвою (home)
.
Ми не хочемо, щоб модальні вікна були доступні на будь-яких підсторінках, лише на головній сторінці, що містить URL. Будь-які підшляхи (як-от /photos/[id]
) не повинні на це впливати. Для цього ми використовуємо групи маршрутів в Next.js, щоб ізолювати макет і слоти, дозволяючи додаткові макети, які не будуть конфліктувати з підкаталогами. Групи маршрутів (окружені дужками) не аналізуються з URL.
src/app/(home)/@modals/default.tsx — на GitHub
Коли макет завантажується вперше, Next.js не має стану для жодного слоту, оскільки він ще не перехопив маршрути динамічно. Замість цього він використає цей файл default.tsx
, що знаходиться в корені директорії слоту.
Оскільки ми не хочемо нічого відображати, ми просто повертаємо null
.
export default function HomePageModalsSlot() {
return null;
}
src/app/(home)/@modals/(.)photos/[id]/page.tsx — на GitHub
Це ваш перехоплений маршрут, де з'явиться наше модальне вікно.
Ця сторінка міститиме елементи дизайну, що належать до модального вікна (наприклад, кнопку закриття), які не повинні з’являтися на сторінці за прямим посиланням.
Коли ви переходите до окремої фотографії, замість використання коду в src/app/photos/[id]/page.tsx
, Next.js використовуватиме цей перехоплювач (interceptor). Користувачі продовжуватимуть свою сесію на поточній сторінці (в нашому прикладі, перегляд усіх фотографій), але слот modals оновиться для вставки діалогового вікна.
Пам’ятайте:
Коли ви створюєте, редагуєте, видаляєте або змінюєте структуру файлів у вашій директорії додатка (особливо з новими слотами та перехоплювачами), потрібно перезапустити Next.js, щоб зміни вступили в силу. Окремі зміни вмісту файлів, як правило, не потребують вручну перезапуску.
Вибір окремої фотографії відобразить діалогове вікно з деталями фотографії поряд.
src/app/photos/[id]/page.tsx — на GitHub
Ця сторінка відображається, коли ви переходите за прямим посиланням на фотографію, або якщо ви оновлюєте фотографію, переглядаючи її в модальному вікні. Вона повинна бути оптимізована для швидкого завантаження та SEO.
Ви помітите, що вона не знаходиться в папці (home)
, тому що ми не хочемо, щоб модальні вікна були доступні на цій сторінці. Якщо ви хочете згрупувати цю сторінку в окрему групу маршрутів, ви можете також назвати файл так: src/app/(photo)/photos/[id]/page.tsx
.
import { photos } from "@/libraries/photos";
import { notFound } from "next/navigation";
export type PhotoPageProps = Readonly<{
params: Promise<{ id: string }>;
}>;
export default async function PhotoPage({ params }: PhotoPageProps) {
const { id } = await params;
const photo = photos[id];
if (!photo) {
notFound();
}
return (
)
}
Під час перегляду діалогу ви можете оновити сторінку, щоб побачити пряме посилання. Ви помітите, що тут немає кнопки Закрити, оскільки зображення не відображається в діалоговому вікні.
Перегляд зображення через пряме посилання.
Закриття модального вікна
Існує два способи закрити модальне вікно.
Кнопка «Назад» у браузері
Коли користувач натискає кнопку «Назад» у браузері, Next.js переключить слот modals
назад до обробника default.tsx
. В нашому прикладі це просто повертає null
, тому модальне вікно зникає. На жаль, наразі немає можливості анімувати закриття модального вікна при натисканні кнопки «Назад», оскільки Next.js не дозволяє вам підключатися до подій браузера.
Виклик router.back()
Також можна створити кнопку закриття в модальному вікні. У діалогах Material UI модальне вікно буде слухати натискання клавіші ESC для виклику події onClose.
Коли подія onClose
спрацьовує, ви захочете створити зворотний виклик (callback), який підключатиметься до маршрутизатора (router) Next.js, щоб повернутися до попередньої сторінки.
"use client";
import { useRouter } from "next/navigation";
const router = useRouter();
...
Поради щодо назв
Перехоплювачі (Interceptors)
Ви повинні вказати правильний шлях для ваших перехоплювачів.
Я мав деякі проблеми, оскільки шлях не міг бути інтерпретований Next.js.
- ✅
src/app/(home)/@modals/(.)photos/[id]/page.tsx
Це правильний формат назви. - ❌
src/app/(home)/@modals/(.)/photos/[id]/page.tsx
Хоча здається, що це працюватиме, зайвий слеш після перехоплювача (interceptor) спричинить проблеми.
Паралельні маршрути (slots)
Оскільки назва вашого слота використовується як змінна у вашому коді, вам слід уникати використання дефісів у назві файлу слота.
- ✅
src/app/(home)/@modals/(.)photos/[id]/page.tsx
Працює, оскількиmodals
— це одне слово. - ✅
src/app/(home)/@photoModals/(.)photos/[id]/page.tsx
Також працює, оскількиphotoModals
написано в camelCase. - ✅
src/app/(home)/@photoModals/(.)photo-view/[id]/page.tsx
Ви також можете використовувати дефіси у вашому шляху. - ❌
src/app/(home)/@photo-modals/(.)photos/[id]/page.tsx
Це спричиняє проблеми, оскількиphoto-modals
не є хорошою змінною в JavaScript/TypeScript.
Також варто зазначити, що ви можете додавати кілька перехоплювачів (interceptors) в один слот. Це може бути корисно для підтримки чистоти структури папок.
src/app/(home)/@modals/(.)photos/**[id]**/page.tsx
src/app/(home)/@modals/(.)photos/**[id]/delete**/page.tsx
src/app/(home)/@modals/(.)photos/**[id]/update**/page.tsx
src/app/(home)/@modals/default.tsx
Один чи кілька слотів?
Чи краще створити один слот чи використовувати кілька слотів для налаштування ваших перехоплювачів? Є варіанти для обох сценаріїв.
Більш ефективно використовувати один слот і додавати кілька перехоплювачів всередині для випадку з модальними вікнами, якщо ви не накладаєте модальні вікна одне на одне.
Розглянемо наступні шляхи:
src/app/(home)/@photoView/(.)photos/[id]/page.tsx
src/app/(home)/@photoCreate/(.)photos/create/page.tsx
Є два окремих слота — photoView
та photoCreate
, і обидва вони відображаються на сторінці одночасно. Коли ви переходите за шляхом /photos/create
, обидва слоти photoView
і photoCreate
будуть відображені — з [id]
, який буде значенням create
. Це не те, що ми хочемо, тому що ми не маємо фотографії з ID create
, що, ймовірно, викличе помилку.
Замість цього зберігайте всі модальні вікна в одному слоті:
src/app/(home)/@modals/(.)photos/[id]/page.tsx
src/app/(home)/@modals/(.)photos/create/page.tsx
Тут шлях після photos/
може бути або з id
, або зі словом create
. Використовуючи один слот, тільки один з двох перехоплювачів буде з’являтися одночасно, і оскільки create
— це літеральне значення, воно має пріоритет над [id]
, динамічним маршрутом.
Висновок
Використовуючи чотири функціональні можливості, які були введені в Next.js 13 (паралельні маршрути, перехоплення маршрутів, динамічні маршрути та групи маршрутів), ви можете створити чудовий користувацький досвід і SEO-дружнє впровадження додатка з модальними вікнами.
Існує багато різних типів модальних вікон та компонентів, з якими ви можете реалізувати цей підхід за допомогою Material UI — цей підручник показав вам, як реалізувати діалогові вікна, але ви також можете застосувати цю саму концепцію для ящиків, меню, спливаючих вікон і вкладок, щоб назвати кілька — або спробуйте реалізувати це з вашою власною системою дизайну.
Перекладено з: Using modals in Next.js with parallel routes (slots), route groups and interceptors