JavaRush /Курсы /Модуль 4: Node.js, Next.js и Angular /Обработка состояний загрузки:

Обработка состояний загрузки: loading.tsx (Suspense Fallback)

Модуль 4: Node.js, Next.js и Angular
9 уровень , 3 лекция
Открыта

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
  
Схема работы Suspense и loading.tsx в Next.js

Пояснение:

  • Если данные уже были закешированы (например, 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 на нужный уровень вложенности.

Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ