12k
All articles

Как работает загрузка данных в Remix 3

Узнайте, как Remix 3 заменяет loaders действиями контроллеров для загрузки данных и как работают маршруты, потоковые Frames, состояния ожидания и ошибки.

OpenReplay Team
OpenReplay Team
Как работает загрузка данных в Remix 3

В Remix 3 данные страницы загружаются внутри действия контроллера (controller action), которое обрабатывает GET-маршрут. Это действие возвращает стандартный веб-объект Response, как правило созданный с помощью context.render().

Если вы уже выпускали приложения на Remix 2, то первым делом наверняка станете искать экспорт loader и хук, который его читает. В новых руководствах их нет, а старые публикации, утверждающие, что загрузчики перекочевали в Remix 3, расходятся с документацией Remix 3.

Remix 3 (v3.0.0), вышедший 1 октября 2026 года, использует модель, отличную от подхода с загрузчиками из Remix 1/2, описанного в этом обзоре исходной архитектуры Remix на основе загрузчиков. В этой статье разберём, где теперь происходит получение данных, как выполняется потоковая передача при первой загрузке и навигации, как устроен UI ожидания и куда попадают ошибки.

Ключевые выводы

  • Remix 3 получает данные страницы внутри действия createController() для GET-маршрута, а действие передаёт эти данные в виде props компоненту, который отрисовывается через context.render().
  • Маршруты объявляются в app/routes.ts, реализуются контроллерами и регистрируются с помощью router.map(). Каждый контроллер отвечает только за непосредственные листья своей карты маршрутов.
  • Frame без fallback задерживает отправку начального HTML, пока его маршрут не ответит. Frame с fallback сначала передаёт в потоке fallback и позднее заменяет его.
  • Ожидаемые сбои, например отсутствующие записи, следует возвращать как объекты Response со статусом 404 или 400. Выброшенное исключение приводит к отклонению промиса router.fetch(), и серверная граница возвращает 500.
  • Если отложенный фрейм завершается с ошибкой после того, как его fallback уже отправлен, ошибка передаётся в механизмы отчётности, а fallback остаётся на экране.

Что заменило загрузчик в Remix 3?

В Remix 3 работу, которую раньше выполнял загрузчик маршрута, делает действие контроллера. Карта маршрутов объявляет URL и HTTP-метод, а createController() привязывает по одному действию к каждому листу маршрута. Действие считывает типизированные значения из context.params, дожидается данных и возвращает Response. Этот паттерн показан в руководстве Routing and Controllers, и экспорты загрузчиков в нём не встречаются вовсе.

// app/actions/albums/controller.tsx
import { createController } from "remix/router";

import { routes } from "../../routes.ts";
// getAlbum and getRecommendations are app data functions, not Remix APIs.
import { getAlbum, getRecommendations } from "./data.ts";
import { AlbumRecommendations } from "./recommendations.tsx";
import { AlbumPage } from "./show-page.tsx";

export default createController(routes.albums, {
  actions: {
    async show(context) {
      // The second argument is an app-defined signal parameter.
      let album = await getAlbum(context.params.albumId, context.request.signal);
      if (!album) return new Response("Album not found", { status: 404 });

      return context.render(<AlbumPage album={album} />);
    },

    async recommendations(context) {
      let album = await getAlbum(context.params.albumId, context.request.signal);
      if (!album) return new Response("Album not found", { status: 404 });

      let recommendations = await getRecommendations(album);
      return context.render(<AlbumRecommendations albums={recommendations} />);
    },
  },
});

Главное отличие от Remix 2 — направление потока данных. В Remix 2 компонент сам «вытягивал» данные через хук. В Remix 3 действие «проталкивает» данные в компонент в виде props. context.render доступен только в том случае, если на роутере установлен middleware render(). Без него действие по-прежнему может вернуть обычный Response или Response.json().

Где происходит загрузка данных в Remix 3?

Приложение на Remix 3 объявляет свои URL и HTTP-методы в app/routes.ts, реализует по одному действию на каждый лист маршрута с помощью createController() и регистрирует каждый контроллер через router.map().

// app/routes.ts
import { get, route } from "remix/routes";

export const routes = route({
  albums: route("/albums", {
    show: get("/:albumId"),
    recommendations: get("/:albumId/recommendations"),
  }),
});

routes.albums.show.href({ albumId: "thriller" }); // "/albums/thriller"

Хелпер href() у каждого маршрута типизирован. Поскольку :albumId — переменная пути, albumId является обязательным аргументом. Если позже вы переименуете паттерн, любой вызов со старым ключом не пройдёт проверку типов.

// app/router.ts
import { createRouter } from "remix/router";

import albumsController from "./actions/albums/controller.tsx";
import { render } from "./middleware/render.tsx"; // the app's own render middleware, built with renderWith()
import { routes } from "./routes.ts";

export const router = createRouter({
  middleware: [render()],
});

router.map(routes.albums, albumsController);

Контроллер отвечает только за непосредственные листья переданной ему карты, поэтому для вложенных карт нужен отдельный вызов router.map(). Remix проверяет каждое сопоставление на этапе инициализации: если у контроллера нет действия для одного из его листьев, инициализация завершится исключением ещё до обработки первого запроса. На практике закладывайте по одному GET-маршруту на страницу плюс по одному на каждую область, которая загружается самостоятельно.

Как работают первая загрузка и навигация в Remix 3?

При первой загрузке middleware рендеринга в Remix 3 передаёт в потоке один HTML-документ. При навигации, после запуска run() в браузере, ссылки и формы с того же источника (same-origin) могут заново заполнять отдельный именованный Frame вместо замены всего документа. Если области нужен собственный запрос, страница отрисовывает её с помощью Frame, указывающего на GET-маршрут этой области. Действие за этим маршрутом возвращает только разметку области, без оболочки документа приложения.

// app/actions/albums/show-page.tsx
import { Frame } from "remix/component";
import type { Handle } from "remix/component";

import { routes } from "../../routes.ts";
import { Document } from "../document.tsx"; // app-defined shell component
import type { Album } from "./data.ts";

export function AlbumPage(handle: Handle<{ album: Album }>) {
  return () => {
    let { album } = handle.props;
    return (
      <Document title={album.title}>
        <h1>{album.title}</h1>
        <Frame
          name="album-recommendations"
          src={routes.albums.recommendations.href({ albumId: album.id })}
          fallback={<p>Loading recommendations…</p>}
        />
      </Document>
    );
  };
}

Frame без fallback является блокирующим: Remix не отправляет первый фрагмент HTML, пока маршрут фрейма не ответит, поэтому страница приходит целиком. Если добавить fallback, он будет отправлен в составе начального HTML, а затем заменён результатом маршрута фрейма, когда соответствующее действие завершится. Выбирайте блокирующий вариант, когда без этой области страница не имеет смысла, и fallback — когда остальную часть страницы стоит показать раньше.

После запуска run() в браузере same-origin ссылки и формы могут заполнять именованный фрейм вместо навигации по всему документу. Эти атрибуты описаны в руководстве Streaming UI with Frames:

АтрибутДействие
data-rmx-targetВыбирает смонтированный именованный фрейм
data-rmx-srcЗадаёт URL запроса, который заполняет фрейм
data-rmx-reset-scroll="false"Сохраняет текущую позицию прокрутки
data-rmx-history="push|replace"Переопределяет поведение истории по умолчанию
data-rmx-documentИсключает ссылку или форму из перехвата

У ссылки адресная строка по-прежнему переходит на href, а data-rmx-src определяет, что именно загружается во фрейм.

<form
  action={routes.albums.recommendations.href({ albumId: album.id })}
  method="get"
  data-rmx-target="album-recommendations"
>
  <button type="submit">Refresh recommendations</button>
</form>

Компоненты также могут перезагружать фреймы самостоятельно. handle.frame.reload() перезагружает фрейм, в котором находится компонент, а await handle.frames.get("album-recommendations")?.reload() обращается к именованному фрейму в другом месте страницы. Если фрейм с таким именем не смонтирован, handle.frames.get() возвращает undefined. Маршруты фреймов должны возвращать только доверенный HTML, поэтому любой недоверенный контент предварительно очищайте на сервере.

Состояния ожидания: fallback и отмена запросов

В Remix 3 UI ожидания реализуется через fallback у Frame и отмену запросов. Fallback у Frame служит начальным индикатором загрузки, а глобальный хук ожидания навигации в руководствах не описан.

Отмену запросов настраивает само приложение. Браузерный резолвер фреймов, который вы передаёте в run(), получает сигнал, и руководства рекомендуют передавать его в fetch(), чтобы запрос фрейма прерывался, когда фрейм удаляется или его вытесняет более новый запрос. На сервере middleware рендеринга передаёт request.signal в renderToStream() и в запросы своих фреймов. Чтобы отмена доходила до вашего собственного кода, передавайте request.signal также в обращения к базе данных и вызовы fetch. Согласно руководству Errors and Cancellation, при прерывании запроса роутер отклоняет промис с request.signal.reason.

Состояния ошибок: ответы и хуки отчётности

В Remix 3 отсутствующую запись или некорректный ввод следует возвращать как Response со статусом 404 или 400. Выброшенное исключение приводит к отклонению промиса router.fetch(), после чего серверная граница отвечает статусом 500. Универсальной компонентной границы ошибок (error boundary) в Remix 3 нет — каждый тип сбоя направляется в своё место:

СбойКуда направляется
Ожидаемый (отсутствующая запись, некорректный ввод)Возвращаемый Response со статусом 404 или 400
Исключение в действии или middlewareПромис router.fetch() отклоняется; серверная граница сообщает об ошибке и возвращает 500
Сбой рендеринга корня или блокирующего фреймаОшибка передаётся в отчётность, поток отклоняется
Сбой отложенного фрейма после отправки fallbackОшибка передаётся в отчётность; fallback остаётся на странице
Браузерная среда выполнения (гидратация, компонент, планировщик, фрейм)Событие error на значении, возвращаемом run()

На серверной границе createRequestListener из remix/node-fetch-server принимает опцию onError. Именно здесь логируются неперехваченные ошибки; кроме того, она может вернуть Response, который будет отправлен вместо стандартного ответа 500. Опция onError у middleware рендеринга и браузерное событие error служат только для отчётности об ошибках. Для фреймов собственный резолвер приложения решает, во что превратится неуспешный ответ: в ограниченный HTML для этой области или в ошибку, передаваемую в упомянутые хуки отчётности. Резолвер из руководств выбрасывает исключение при любом статусе, отличном от OK, поэтому о каждом неудачном запросе фрейма поступает сообщение.

Если отложенный фрейм завершается с ошибкой после того, как его fallback уже отправлен, сервер не может заменить страницу ответом с ошибкой, поскольку байты тела ответа уже переданы клиенту. Ошибка передаётся в отчётность, а fallback остаётся на экране. Серверный лог зафиксирует эту ошибку, но не покажет, что видел пользователь. Этот пробел закрывает session replay: вы увидите, как пользователи смотрят на так и не исчезнувшее сообщение «Loading…», продолжая работать с остальной частью страницы. Пишите fallback так, чтобы зависшая область оставалась понятной, даже если она так и не заполнится.

Миграция с Remix 2

Перевод приложения с Remix 2 на Remix 3 — это переписывание, а не обновление. Маршруты переезжают в типизированную карту маршрутов, загрузка данных — в действия контроллеров, а UI — на собственную компонентную среду выполнения Remix из remix/component. Раздела о миграции в руководствах нет. Путь обновления «на месте» для Remix 2 — это React Router v7, а Remix 3 — отдельная кодовая база. Материалы вроде этого руководства по переходу с Next.js на Remix, написанного для Remix 1/2, рассматривайте лишь как справочный контекст, а не как руководство по Remix 3.

Заключение

Загрузка данных в Remix 3 сводится к следующему: GET-маршрут, действие контроллера, которое дожидается данных и возвращает Response, и фреймы для областей, которые загружаются или завершаются с ошибкой самостоятельно. Чтобы начать, опишите маршруты одной страницы в app/routes.ts, перенесите получение её данных в контроллер и решите, каким областям нужен fallback. Подробнее о работе с новым фреймворком читайте в статье о том, как Remix 3 спроектирован с расчётом на AI-агентов для написания кода.

Часто задаваемые вопросы

Использует ли Remix 3 React?

Нет. Remix 3 сохраняет синтаксис JSX, но не работает на React или React Router. Он использует собственную компонентную модель, экспортируемую из remix/component. Компонент состоит из внешней функции настройки, которая получает handle и выполняется один раз, и внутренней функции рендеринга, которая выполняется при каждом обновлении. Хуки React, такие как useState, не входят в эту модель, поэтому библиотеки компонентов React без доработки в Remix 3 не работают.

Как работают отправка форм и мутации в Remix 3?

Объявите форму с помощью хелпера маршрутов form() в app/routes.ts. По умолчанию он создаёт два листа по одному и тому же URL: index-маршрут GET, который отображает форму, и action-маршрут POST, который обрабатывает отправку. Оба листа принадлежат одному контроллеру. После успешного создания, обновления или удаления руководства Remix 3 рекомендуют возвращать редирект 303, чтобы обновление страницы не приводило к повторной отправке формы.

Может ли действие контроллера в Remix 3 возвращать JSON вместо HTML?

Да. Действие контроллера может вернуть любой веб-объект Response, включая Response.json(), редиректы, файлы и потоки. Руководство Remix 3 Forms and Mutations предлагает использовать JSON для таких сценариев, как автодополнение, поллинг, автономные виджеты, API и взаимодействие между сервисами. При этом оно не рекомендует добавлять отдельный JSON-путь для мутаций в форму, которая уже работает через HTML-ответ и редирект.

Remix 3 — это то же самое, что React Router v7?

Нет. Remix v2 был объединён с React Router v7, вышедшим в ноябре 2024 года, в качестве его Framework Mode. Это путь обновления для приложений на Remix 2, которые хотят сохранить загрузчики и React. Remix 3 — отдельный проект, анонсированный в мае 2025 года и построенный на собственной компонентной модели. Переход на него означает переписывание маршрутов, загрузки данных и UI.

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.