Cómo funciona la carga de datos en Remix 3
Vea cómo Remix 3 sustituye los loaders por acciones de controlador para cargar datos y cómo funcionan las rutas, los Frames en streaming, los estados pendientes y los errores.
Remix 3 carga los datos de una página dentro de una acción de controlador que gestiona una ruta GET, y esa acción devuelve un Response web estándar, normalmente generado por context.render().
Si ya has puesto en producción una aplicación con Remix 2, lo primero que probablemente harás será buscar el export loader y el hook que lo lee. No los encontrarás en las nuevas guías, y los artículos antiguos que afirman que los loaders se mantuvieron en Remix 3 no coinciden con lo que muestra la documentación de Remix 3.
Remix 3 (v3.0.0), publicado el 1 de octubre de 2026, utiliza un modelo distinto del enfoque basado en loaders de Remix 1/2 descrito en esta introducción a la arquitectura original de Remix basada en loaders. Este artículo explica dónde se realiza ahora la obtención de datos, cómo se transmiten en streaming la carga inicial y las navegaciones, cómo funciona la UI de estados pendientes y adónde van los errores.
Puntos clave
- Remix 3 obtiene los datos de la página dentro de una acción de
createController()para una ruta GET, y la acción pasa esos datos como props al componente que renderiza concontext.render(). - Las rutas se declaran en
app/routes.ts, se implementan mediante controladores y se registran conrouter.map(). Cada controlador gestiona únicamente las hojas directas de su mapa de rutas. - Un
Framesinfallbackretiene el HTML inicial hasta que su ruta responde. UnFrameconfallbackenvía primero ese fallback en streaming y lo reemplaza después. - Los fallos esperados, como los registros inexistentes, deben devolverse como objetos
Responsecon un estado 404 o 400. Un error lanzado hace querouter.fetch()se rechace, y el boundary del servidor devuelve un 500. - Cuando un frame diferido falla después de haberse enviado su fallback, el error se reporta y el fallback permanece en pantalla.
¿Qué sustituyó al loader en Remix 3?
En Remix 3, una acción de controlador hace el trabajo que antes hacía un loader de ruta. El mapa de rutas declara una URL y un método HTTP, y createController() vincula una acción a cada hoja de ruta. La acción lee valores tipados de context.params, espera a que se resuelvan tus datos y devuelve un Response. La guía de Routing and Controllers muestra este patrón, y en ella no aparece ningún export de loader.
// 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} />);
},
},
});
El cambio principal respecto a Remix 2 es la dirección del flujo de datos. En Remix 2, el componente obtenía sus datos a través de un hook. En Remix 3, la acción inyecta los datos en el componente como props. context.render solo existe cuando el middleware render() está instalado en el router. Sin él, una acción puede seguir devolviendo un Response simple o Response.json().
¿Dónde se realiza la carga de datos en Remix 3?
Una aplicación de Remix 3 declara sus URL y métodos HTTP en app/routes.ts, implementa una acción por cada hoja de ruta con createController() y registra cada controlador con 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"
El helper href() de cada ruta está tipado. Como :albumId es una variable de ruta, albumId es un argumento obligatorio. Si más adelante renombras el patrón, cualquier llamada que siga pasando la clave antigua no superará la comprobación de tipos.
// 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 controlador solo gestiona las hojas directas del mapa que le pasas, por lo que los mapas anidados necesitan su propia llamada a router.map(). Remix verifica cada mapeo durante la configuración: si un controlador no tiene acción para alguna de sus hojas, la configuración lanza un error antes de atender cualquier petición. En la práctica, conviene prever una ruta GET por página, más otra por cada región que se cargue de forma independiente.
¿Cómo funcionan la carga inicial y la navegación en Remix 3?
En la carga inicial, el middleware de renderizado de Remix 3 transmite en streaming un único documento HTML. En la navegación, una vez que run() se inicia en el navegador, los enlaces y formularios del mismo origen pueden volver a rellenar un único Frame con nombre en lugar de reemplazar el documento completo. Cuando una región necesita su propia petición, la página la renderiza con Frame, apuntando a la ruta GET de esa región. La acción asociada a esa ruta devuelve únicamente el marcado de la región y omite el shell del documento de la aplicación.
// 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 sin fallback es bloqueante: Remix no envía el primer fragmento de HTML hasta que la ruta del frame ha respondido, de modo que la página llega completa. Si añades un fallback, este se incluye en el HTML inicial y después se sustituye por la salida de la ruta del frame cuando esa acción termina. Elige el modo bloqueante cuando la página no tenga sentido sin la región, y un fallback cuando valga la pena mostrar primero el resto de la página.
Una vez que run() se inicia en el navegador, los enlaces y formularios del mismo origen pueden rellenar un frame con nombre en lugar de navegar por todo el documento. Estos atributos están documentados en Streaming UI with Frames:
| Atributo | Efecto |
|---|---|
data-rmx-target | Selecciona un frame con nombre que esté montado |
data-rmx-src | Establece la URL de la petición que rellena el frame |
data-rmx-reset-scroll="false" | Mantiene la posición de scroll actual |
data-rmx-history="push|replace" | Sobrescribe el comportamiento predeterminado del historial |
data-rmx-document | Excluye el enlace o formulario de la interceptación |
En un enlace, la barra de direcciones sigue cambiando a href, mientras que data-rmx-src determina qué se carga en el frame.
<form
action={routes.albums.recommendations.href({ albumId: album.id })}
method="get"
data-rmx-target="album-recommendations"
>
<button type="submit">Refresh recommendations</button>
</form>
Los componentes también pueden recargar frames por sí mismos. handle.frame.reload() recarga el frame que contiene al componente, y await handle.frames.get("album-recommendations")?.reload() apunta a un frame con nombre situado en otra parte de la página. Si no hay ningún frame montado con ese nombre, handle.frames.get() devuelve undefined. Las rutas de frames solo deben devolver HTML de confianza, así que sanea primero en el servidor cualquier contenido que no lo sea.
Estados pendientes: fallbacks y cancelación
Remix 3 gestiona la UI de estados pendientes mediante los fallbacks de Frame y la cancelación de peticiones. El fallback de Frame es la UI de carga inicial, y las guías no documentan ningún hook global de navegación pendiente.
La cancelación es algo que debe configurar la propia aplicación. El resolver de frames del navegador que pasas a run() recibe una señal, y las guías indican que se la pases a fetch() para que la petición de un frame se detenga cuando dicho frame se elimine o cuando una petición más reciente lo reemplace. En el servidor, el middleware de renderizado pasa request.signal a renderToStream() y a sus peticiones de frames. Para que la cancelación llegue a tu propio código, pasa también request.signal a las llamadas a la base de datos y a fetch. Según Errors and Cancellation, cuando una petición se aborta, el router se rechaza con request.signal.reason.
Estados de error: respuestas y hooks de reporte
En Remix 3, devuelve un registro inexistente o una entrada no válida como un Response con estado 404 o 400. Un error lanzado hace que router.fetch() se rechace, y el boundary del servidor responde entonces con un 500. Remix 3 no tiene un error boundary de componentes que lo capture todo; cada tipo de fallo tiene su propio destino:
| Fallo | Adónde va |
|---|---|
| Esperado (registro inexistente, entrada incorrecta) | Se devuelve un Response con estado 404 o 400 |
| Error lanzado en una acción o middleware | router.fetch() se rechaza; el boundary del servidor lo reporta y devuelve un 500 |
| Fallo de renderizado en la raíz o en un frame bloqueante | El error se reporta y el stream se rechaza |
| Fallo de un frame diferido después de enviar el fallback | El error se reporta; el fallback permanece en la página |
| Runtime del navegador (hidratación, componente, scheduler, frame) | Evento error en el valor devuelto por run() |
En el boundary del servidor, createRequestListener de remix/node-fetch-server acepta una opción onError. Es donde se registran los errores no capturados y, opcionalmente, puede devolver un Response que se enviará en lugar del 500 predeterminado. La opción onError del middleware de renderizado y el evento error del navegador solo reportan errores. En el caso de los frames, es el resolver de tu propia aplicación el que decide en qué se convierte una respuesta fallida: HTML acotado para esa región o un error enviado a esos hooks de reporte. El resolver de las guías lanza un error ante cualquier estado que no sea OK, por lo que toda petición de frame fallida se reporta.
Cuando un frame diferido falla después de que su fallback ya se haya enviado, el servidor no puede sustituir la página por una respuesta de error, porque los bytes del cuerpo ya se han transmitido. El error se reporta y el fallback permanece en pantalla. Un log del servidor registra ese error, pero no muestra lo que vio el usuario. Session replay cubre ese vacío: puedes ver a los usuarios frente a un mensaje de “Cargando…” que nunca se resolvió mientras seguían usando el resto de la página. Redacta los fallbacks de forma que una región bloqueada siga teniendo sentido aunque nunca llegue a rellenarse.
Migración desde Remix 2
Pasar una aplicación de Remix 2 a Remix 3 es una reescritura, no una actualización. Las rutas pasan a un mapa de rutas tipado, la carga de datos pasa a las acciones de los controladores y la UI pasa al runtime de componentes propio de Remix en remix/component. Las guías no incluyen ningún capítulo de migración. La vía de actualización directa de Remix 2 es React Router v7, y Remix 3 es una base de código independiente. Considera guías como este recorrido de Next.js a Remix, escrito para Remix 1/2, solo como contexto, no como una guía de Remix 3.
Conclusión
La carga de datos en Remix 3 se reduce a esto: una ruta GET, una acción de controlador que espera los datos y devuelve un Response, y frames para las regiones que se cargan o fallan de forma independiente. Para empezar, mapea las rutas de una página en app/routes.ts, traslada su obtención de datos a un controlador y decide qué regiones necesitan un fallback. Para saber más sobre cómo trabajar con el nuevo framework, consulta cómo está diseñado Remix 3 para los agentes de programación con IA.
Preguntas frecuentes
¿Remix 3 usa React?
No. Remix 3 mantiene la sintaxis JSX, pero no se ejecuta sobre React ni React Router. Utiliza su propio modelo de componentes, exportado desde remix/component. Un componente es una función de configuración externa que recibe un handle y se ejecuta una sola vez, más una función de renderizado interna que se ejecuta en cada actualización. Los hooks de React como useState no forman parte de este modelo, por lo que las bibliotecas de componentes de React no funcionan en Remix 3 sin modificaciones.
¿Cómo funcionan los envíos de formularios y las mutaciones en Remix 3?
Declara el formulario con el helper de rutas form() en app/routes.ts. De forma predeterminada, crea dos hojas en la misma URL: una ruta GET index que muestra el formulario y una ruta POST action que gestiona el envío. Ambas hojas pertenecen al mismo controlador. Tras una creación, actualización o eliminación correcta, las guías de Remix 3 recomiendan devolver una redirección 303 para que al recargar la página no se vuelva a enviar el formulario.
¿Puede una acción de controlador de Remix 3 devolver JSON en lugar de HTML?
Sí. Una acción de controlador puede devolver cualquier Response web, incluidos Response.json(), redirecciones, archivos y streams. La guía Forms and Mutations de Remix 3 sugiere usar JSON en casos como el autocompletado, el polling, los widgets autocontenidos, las API y las llamadas entre servicios. Desaconseja añadir una vía de mutación JSON independiente para un formulario que ya funciona mediante una respuesta HTML y una redirección.
¿Remix 3 es lo mismo que React Router v7?
No. Remix v2 se fusionó con React Router v7, publicado en noviembre de 2024, como su Framework Mode. Esa es la vía de actualización para las aplicaciones de Remix 2 que quieran conservar los loaders y React. Remix 3 es un proyecto independiente, anunciado en mayo de 2025 y construido sobre su propio modelo de componentes. Migrar a él implica reescribir las rutas, la carga de datos y la UI.
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