12k
All articles

Comment fonctionne le chargement des données dans Remix 3

Découvrez comment Remix 3 remplace les loaders par des actions de contrôleur pour charger les données, avec les routes, Frames en streaming, états d’attente et erreurs.

OpenReplay Team
OpenReplay Team
Comment fonctionne le chargement des données dans Remix 3

Remix 3 charge les données d’une page dans une action de contrôleur qui gère une route GET. Cette action renvoie une Response Web standard, généralement produite par context.render().

Si vous avez déjà mis en production une application Remix 2, votre premier réflexe sera sans doute de chercher l’export loader et le hook qui permet de le lire. Vous ne les trouverez pas dans les nouveaux guides. Les anciens articles qui affirment que les loaders ont été repris dans Remix 3 ne correspondent pas à ce que montre la documentation de Remix 3.

Remix 3 (v3.0.0), publié le 1er octobre 2026, repose sur un modèle différent de l’approche par loaders de Remix 1/2 décrite dans cette présentation de l’architecture originelle de Remix, fondée sur les loaders. Cet article explique où se trouve désormais la récupération des données, comment le premier chargement et les navigations sont diffusés en streaming, comment fonctionne l’interface d’attente et où sont acheminées les erreurs.

Points clés à retenir

  • Remix 3 récupère les données d’une page dans une action createController() associée à une route GET. L’action transmet ces données sous forme de props au composant qu’elle rend avec context.render().
  • Les routes sont déclarées dans app/routes.ts, implémentées par des contrôleurs et enregistrées avec router.map(). Chaque contrôleur ne gère que les feuilles directes de sa table de routes.
  • Un Frame sans fallback retient le HTML initial jusqu’à ce que sa route ait répondu. Un Frame doté d’un fallback diffuse d’abord ce contenu de repli, puis le remplace ensuite.
  • Les échecs attendus, comme les enregistrements introuvables, doivent être renvoyés sous forme d’objets Response avec un statut 404 ou 400. Une erreur levée fait échouer la promesse de router.fetch(), et la frontière serveur renvoie alors une erreur 500.
  • Lorsqu’un frame différé échoue après l’envoi de son contenu de repli, l’erreur est signalée et le contenu de repli reste affiché.

Qu’est-ce qui remplace le loader dans Remix 3 ?

Dans Remix 3, une action de contrôleur remplit le rôle autrefois dévolu au loader d’une route. La table de routes déclare une URL et une méthode HTTP, et createController() associe une action à chaque feuille de route. L’action lit des valeurs typées depuis context.params, attend vos données avec await et renvoie une Response. Le guide Routing and Controllers illustre ce modèle, et aucun export de loader n’y apparaît.

// 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} />);
    },
  },
});

Le principal changement par rapport à Remix 2 concerne le sens du flux de données. Dans Remix 2, le composant récupérait ses données via un hook. Dans Remix 3, c’est l’action qui pousse les données vers le composant sous forme de props. context.render n’existe que si le middleware render() est installé sur le routeur. Sans lui, une action peut toujours renvoyer une simple Response ou Response.json().

Où se trouve le chargement des données dans Remix 3 ?

Une application Remix 3 déclare ses URL et ses méthodes HTTP dans app/routes.ts, implémente une action par feuille de route avec createController() et enregistre chaque contrôleur avec 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"

Le helper href() de chaque route est typé. Comme :albumId est une variable de chemin, albumId est un argument obligatoire. Si vous renommez le motif par la suite, tout appel qui transmet encore l’ancienne clé échouera à la vérification de types.

// 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);

Un contrôleur ne gère que les feuilles directes de la table que vous lui transmettez : les tables imbriquées nécessitent donc leur propre appel à router.map(). Remix vérifie chaque association lors de l’initialisation : si un contrôleur ne définit pas d’action pour l’une de ses feuilles, l’initialisation lève une erreur avant même que la moindre requête soit servie. En pratique, prévoyez une route GET par page, plus une route supplémentaire pour chaque zone qui se charge de manière indépendante.

Comment fonctionnent le premier chargement et la navigation dans Remix 3 ?

Au premier chargement, le middleware de rendu de Remix 3 diffuse en streaming un unique document HTML. Lors de la navigation, une fois run() lancé dans le navigateur, les liens et formulaires de même origine peuvent recharger un seul Frame nommé au lieu de remplacer l’intégralité du document. Lorsqu’une zone nécessite sa propre requête, la page la rend avec Frame, en le faisant pointer vers la route GET de cette zone. L’action associée à cette route ne renvoie que le balisage de la zone, sans le shell du document de l’application.

// 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>
    );
  };
}

Un Frame sans fallback est bloquant : Remix n’envoie pas le premier fragment de HTML tant que la route du frame n’a pas répondu, si bien que la page arrive complète. Si vous ajoutez un fallback, celui-ci est au contraire inclus dans le HTML initial, puis remplacé par le résultat de la route du frame une fois l’action terminée. Optez pour le mode bloquant lorsque la page n’a aucun sens sans cette zone, et pour un contenu de repli lorsque le reste de la page mérite d’être affiché en premier.

Une fois run() lancé dans le navigateur, les liens et formulaires de même origine peuvent alimenter un frame nommé au lieu de déclencher une navigation sur l’ensemble du document. Ces attributs sont documentés dans Streaming UI with Frames :

AttributEffet
data-rmx-targetSélectionne un frame nommé monté
data-rmx-srcDéfinit l’URL de la requête qui alimente le frame
data-rmx-reset-scroll="false"Conserve la position de défilement actuelle
data-rmx-history="push|replace"Remplace le comportement par défaut de l’historique
data-rmx-documentExclut le lien ou le formulaire de l’interception

Sur un lien, la barre d’adresse se met tout de même à jour vers href, tandis que data-rmx-src détermine ce qui est chargé dans le frame.

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

Les composants peuvent également recharger eux-mêmes des frames. handle.frame.reload() recharge le frame qui contient le composant, et await handle.frames.get("album-recommendations")?.reload() cible un frame nommé situé ailleurs dans la page. Si aucun frame portant ce nom n’est monté, handle.frames.get() renvoie undefined. Les routes de frames ne doivent renvoyer que du HTML de confiance : nettoyez donc au préalable tout contenu non fiable côté serveur.

États d’attente : contenus de repli et annulation

Remix 3 gère l’interface d’attente au moyen des contenus de repli de Frame et de l’annulation des requêtes. Le fallback de Frame constitue l’interface de chargement initiale, et les guides ne documentent aucun hook global d’attente de navigation.

L’annulation doit être câblée par l’application. Le resolver de frames côté navigateur que vous transmettez à run() reçoit un signal, et les guides recommandent de le passer à fetch() afin qu’une requête de frame s’interrompe lorsque son frame est supprimé ou qu’une requête plus récente la remplace. Côté serveur, le middleware de rendu transmet request.signal à renderToStream() ainsi qu’à ses requêtes de frames. Pour que l’annulation atteigne votre propre code, transmettez également request.signal à vos appels de base de données et à vos appels fetch. Selon Errors and Cancellation, lorsqu’une requête est interrompue, le routeur rejette la promesse avec request.signal.reason.

États d’erreur : réponses et hooks de signalement

Dans Remix 3, renvoyez un enregistrement introuvable ou une saisie invalide sous forme de Response avec un statut 404 ou 400. Une erreur levée fait échouer la promesse de router.fetch(), et la frontière serveur répond alors avec une erreur 500. Remix 3 ne propose pas de frontière d’erreur (error boundary) universelle au niveau des composants : chaque type d’échec est acheminé vers sa propre destination.

ÉchecDestination
Attendu (enregistrement introuvable, saisie invalide)Response renvoyée avec un statut 404 ou 400
Erreur levée dans une action ou un middlewareLa promesse de router.fetch() est rejetée ; la frontière serveur signale l’erreur et renvoie une erreur 500
Échec du rendu racine ou d’un frame bloquantL’erreur est signalée et le flux est rejeté
Échec d’un frame différé après l’envoi du contenu de repliL’erreur est signalée ; le contenu de repli reste dans la page
Runtime navigateur (hydratation, composant, ordonnanceur, frame)Événement error sur la valeur renvoyée par run()

Au niveau de la frontière serveur, createRequestListener de remix/node-fetch-server accepte une option onError. C’est là que sont journalisées les erreurs non interceptées, et cette option peut éventuellement renvoyer une Response à envoyer à la place de l’erreur 500 par défaut. L’option onError du middleware de rendu et l’événement error côté navigateur se contentent de signaler les erreurs. Pour les frames, c’est le resolver de votre application qui détermine ce que devient une réponse en échec : du HTML circonscrit à la zone concernée, ou une erreur transmise à ces hooks de signalement. Le resolver présenté dans les guides lève une erreur pour tout statut autre que OK, de sorte que chaque requête de frame en échec est signalée.

Lorsqu’un frame différé échoue alors que son contenu de repli a déjà été envoyé, le serveur ne peut plus remplacer la page par une réponse d’erreur, car des octets du corps de la réponse ont déjà été transmis. L’erreur est signalée et le contenu de repli reste affiché. Un journal serveur consigne bien cette erreur, mais il ne montre pas ce que l’utilisateur a vu. La relecture de session (session replay) comble cette lacune : vous pouvez observer des utilisateurs bloqués devant un message « Loading… » qui ne s’est jamais résolu, tout en continuant à utiliser le reste de la page. Rédigez vos contenus de repli de sorte qu’une zone bloquée reste compréhensible même si elle ne se remplit jamais.

Migrer depuis Remix 2

Passer d’une application Remix 2 à Remix 3 relève de la réécriture, et non d’une simple mise à niveau. Les routes sont déplacées dans une table de routes typée, le chargement des données passe dans des actions de contrôleur, et l’interface utilisateur repose désormais sur le runtime de composants propre à Remix, dans remix/component. Les guides ne comportent aucun chapitre consacré à la migration. La voie de mise à niveau directe de Remix 2 est React Router v7, tandis que Remix 3 est une base de code distincte. Considérez des guides tels que ce tutoriel de migration de Next.js vers Remix, rédigé pour Remix 1/2, comme de simples ressources de contexte, et non comme des guides pour Remix 3.

Conclusion

Le chargement des données dans Remix 3 se résume à ceci : une route GET, une action de contrôleur qui attend les données et renvoie une Response, et des frames pour les zones qui se chargent ou échouent de manière indépendante. Pour commencer, déclarez les routes d’une page dans app/routes.ts, déplacez la récupération de ses données dans un contrôleur et déterminez quelles zones nécessitent un contenu de repli. Pour en savoir plus sur l’utilisation de ce nouveau framework, découvrez comment Remix 3 est conçu pour les agents de codage IA.

FAQ

Remix 3 utilise-t-il React ?

Non. Remix 3 conserve la syntaxe JSX, mais ne s'exécute ni sur React ni sur React Router. Il utilise son propre modèle de composants, exporté depuis remix/component. Un composant se compose d'une fonction d'initialisation externe, qui reçoit un handle et ne s'exécute qu'une seule fois, et d'une fonction de rendu interne, qui s'exécute à chaque mise à jour. Les hooks React tels que useState ne font pas partie de ce modèle : les bibliothèques de composants React ne fonctionnent donc pas dans Remix 3 sans adaptation.

Comment fonctionnent les soumissions de formulaires et les mutations dans Remix 3 ?

Déclarez le formulaire avec le helper de route form() dans app/routes.ts. Par défaut, il crée deux feuilles sur la même URL : une route GET index qui affiche le formulaire et une route POST action qui traite la soumission. Les deux feuilles appartiennent au même contrôleur. Après une création, une mise à jour ou une suppression réussie, les guides de Remix 3 recommandent de renvoyer une redirection 303 afin que l'actualisation de la page ne soumette pas à nouveau le formulaire.

Une action de contrôleur Remix 3 peut-elle renvoyer du JSON plutôt que du HTML ?

Oui. Une action de contrôleur peut renvoyer n'importe quelle Response Web, y compris Response.json(), des redirections, des fichiers et des flux. Le guide Forms and Mutations de Remix 3 suggère le JSON pour des cas tels que l'autocomplétion, le polling, les widgets autonomes, les API et les appels entre services. Il déconseille en revanche d'ajouter un chemin de mutation JSON distinct pour un formulaire qui fonctionne déjà au moyen d'une réponse HTML et d'une redirection.

Remix 3 est-il identique à React Router v7 ?

Non. Remix v2 a été fusionné dans React Router v7, publié en novembre 2024, sous la forme de son Framework Mode. C'est la voie de mise à niveau pour les applications Remix 2 qui souhaitent conserver les loaders et React. Remix 3 est un projet distinct, annoncé en mai 2025 et fondé sur son propre modèle de composants. Y migrer implique de réécrire les routes, le chargement des données et l'interface utilisateur.

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.