1. Введение
Давайте честно: ожидание — мучительно. Особенно для пользователей, которые привыкли к молниеносным сайтам и приложениям. Никто не любит смотреть на пустой экран и гадать, «сломалось или просто долго грузится?». Хорошее приложение всегда сообщает пользователю, что идет загрузка — будь то крутящийся спиннер, скелетон, надпись "Загружаем..." или даже милый котик (но это уже на ваше усмотрение).
В Next.js 15 с App Router обработка состояний загрузки реализована очень просто — через файл loading.tsx, который автоматически работает как Suspense Fallback. О, да, никакой магии, только немного волшебства React и Next.js!
Что такое Suspense?
В React Suspense — это механизм, который позволяет «отложить» рендеринг части интерфейса, пока не будут загружены нужные данные или компоненты. Пока что-то грузится, показывается fallback — специальная «заглушка». В Next.js 15 с App Router Suspense встроен «из коробки», и вам не нужно явно писать <Suspense fallback={...}> для большинства случаев: Next сам делает это за вас.
Как работает loading.tsx?
Когда вы создаёте файл loading.tsx (или loading.jsx/loading.ts/loading.js) в папке маршрута, Next.js автоматически использует его как fallback для Suspense — то есть вместо вашей страницы или компонента, пока идет загрузка данных, будет показано содержимое loading.tsx.
Аналогия:
Представьте, что вы заказали пиццу. Пока она готовится, вам приносят бесплатный хлебушек и воду — чтобы вы не умерли со скуки и не ушли в другой ресторан. Вот этот хлебушек — ваш loading.tsx.
2. Где размещать loading.tsx и как он работает
Структура папок
В Next.js 15 с App Router структура папок определяет маршруты. В каждом маршруте (или даже в layout-группе) вы можете разместить свой loading.tsx. Вот пример структуры:
/app
/dashboard
page.tsx
loading.tsx
layout.tsx
/profile
page.tsx
loading.tsx
Пояснение:
- Если пользователь открывает /dashboard, а данные для этой страницы ещё не пришли, Next.js покажет содержимое /app/dashboard/loading.tsx.
- Если загрузка затрагивает только часть страницы (например, вложенный маршрут), то можно сделать отдельный loading.tsx для этой вложенной папки.
Когда именно показывается loading.tsx?
Пояснение:
- Когда Server Component внутри маршрута вызывает асинхронную функцию (например, await fetch(...)), Next.js автоматически включает Suspense и показывает loading.tsx до тех пор, пока данные не будут загружены.
- Если загрузка данных происходит на клиенте (например, через Client Component и useEffect), то loading.tsx не сработает — тут уже придётся показывать спиннер вручную.
3. Пример: добавляем загрузку в наше приложение
Давайте продолжим развивать простое приложение «Список задач», которое мы строим по ходу курса. Пусть у нас есть маршрут /tasks, который выводит список задач с сервера.
Шаг 1: Структура папки
/app
/tasks
page.tsx
loading.tsx
Шаг 2: Пример page.tsx
// app/tasks/page.tsx
export default async function TasksPage() {
const res = await fetch('https://jsonplaceholder.typicode.com/todos?_limit=5');
const tasks = await res.json();
return (
<div>
<h1>Список задач</h1>
<ul>
{tasks.map((task: any) => (
<li key={task.id}>
<input type="checkbox" checked={task.completed} readOnly />
{task.title}
</li>
))}
</ul>
</div>
);
}
Здесь мы делаем асинхронный запрос к серверу. Если сервер думает дольше пары миллисекунд, пользователь увидит... что? Пока ничего! Давайте это исправим.
Шаг 3: Пример loading.tsx
// app/tasks/loading.tsx
export default function Loading() {
return (
<div style={ { padding: 24, textAlign: 'center' } }>
<div className="spinner" />
<p>Загружаем задачи...</p>
</div>
);
}
Можно добавить спиннер с помощью CSS, или просто оставить текст — главное, чтобы пользователь понял: процесс идёт.
Шаг 4: (Необязательно) Добавляем простой CSS-спиннер
/* app/tasks/loading.module.css */
.spinner {
width: 40px;
height: 40px;
border: 5px solid #ddd;
border-top: 5px solid #0070f3;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 16px auto;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
И используем класс:
import styles from './loading.module.css';
export default function Loading() {
return (
<div style={ { padding: 24, textAlign: 'center' } }>
<div className={styles.spinner} />
<p>Загружаем задачи...</p>
</div>
);
}
4. Как работает Suspense и когда показывается loading.tsx
Визуальная схема
flowchart TD
A[Пользователь заходит на /tasks] --> B{Данные готовы?}
B -- Да --> C[Показываем page.tsx]
B -- Нет --> D[Показываем loading.tsx]
D -->|Данные пришли| C
Пояснение:
- Если данные уже были закешированы (например, cache: "force-cache"), загрузка может быть мгновенной, и loading.tsx почти не покажется.
- Если вы используете cache: "no-store" или данные всегда свежие, пользователь увидит loading.tsx на время загрузки.
Вложенные маршруты и loading.tsx
Вложенные маршруты могут иметь свои собственные файлы loading.tsx. Next.js покажет только тот fallback, который соответствует «самой глубокой» загружаемой части.
/app
/dashboard
layout.tsx
loading.tsx
/analytics
page.tsx
loading.tsx
Пояснение:
Если грузится только /dashboard/analytics, то сначала покажется dashboard/analytics/loading.tsx, а если там его нет — dashboard/loading.tsx.
5. Сложные случаи: Suspense внутри компонента
Иногда хочется показать спиннер только для части страницы, а не для всего маршрута. Для этого можно использовать Suspense внутри компонента.
import { Suspense } from 'react';
import TasksList from './TasksList';
export default function TasksPage() {
return (
<div>
<h1>Список задач</h1>
<Suspense fallback={<p>Загружаем задачи...</p>}>
<TasksList />
</Suspense>
</div>
);
}
В этом случае TasksList должен быть асинхронным компонентом (Server Component), который делает fetch. Suspense сработает только для него, а остальная страница будет видна сразу.
Важно:
Suspense внутри Client Components работает только для динамического импорта компонентов, а не для загрузки данных (пока что). Для данных Suspense работает только в Server Components и App Router.
6. Практика: добавляем loading.tsx в реальный проект
Допустим, у вас есть проект с несколькими маршрутами:
/app
/tasks
page.tsx
loading.tsx
/profile
page.tsx
loading.tsx
layout.tsx
Добавьте в каждый маршрут свой loading.tsx, чтобы пользователь всегда видел индикатор загрузки при переходе между страницами. Можно сделать разные loading-компоненты — для задач показывать «Загружаем задачи...», для профиля — «Загружаем профиль...», а для layout — общий спиннер.
7. Типичные ошибки при работе с loading.tsx и Suspense
Ошибка №1: loading.tsx не появляется при загрузке
Самая частая причина — вы загружаете данные не в Server Component, а в Client Component через useEffect. Suspense и loading.tsx работают только для асинхронных Server Components! Если вы делаете fetch внутри useEffect, Next.js не может «знать» о загрузке на сервере.
Ошибка №2: loading.tsx не в той папке
Если вы положили loading.tsx не в ту папку (например, не в папку маршрута, а в корень), он не будет работать. loading.tsx должен находиться в той же папке, что и page.tsx или layout.tsx, для которых вы хотите показывать fallback.
Ошибка №3: loading.tsx слишком «тяжёлый»
Иногда новички добавляют в loading.tsx сложную логику, тяжелые запросы или даже новые fetch-запросы. Не надо! loading.tsx должен быть максимально простым, быстрым и не вызывать новых асинхронных операций — иначе он сам может «подвиснуть».
Ошибка №4: неочевидный UX
Пользователь не понимает, что происходит: спиннер слишком маленький, нет текста, или наоборот — слишком яркий и раздражающий. Всегда думайте о пользователе: loading.tsx — это не место для сюрпризов, а для дружелюбного ожидания.
Ошибка №5: забыли про вложенные маршруты
Если у вас есть вложенные маршруты, а loading.tsx только на верхнем уровне, при загрузке вложенных данных может показываться не тот fallback, который вы ожидаете. Добавляйте loading.tsx на нужный уровень вложенности.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ