12k
All articles

Como funciona o carregamento de dados no Remix 3

Veja como o Remix 3 substitui loaders por ações de controlador para carregar dados e como funcionam rotas, Frames em streaming, estados pendentes e erros.

OpenReplay Team
OpenReplay Team
Como funciona o carregamento de dados no Remix 3

O Remix 3 carrega os dados da página dentro de uma action de controller que trata uma rota GET, e essa action retorna um Response padrão da Web, normalmente gerado por context.render().

Se você já colocou um app Remix 2 em produção, seu primeiro instinto provavelmente será procurar o export loader e o hook que o lê. Você não vai encontrá-los nos novos guias, e posts mais antigos que afirmam que os loaders foram mantidos no Remix 3 não correspondem ao que a documentação do Remix 3 mostra.

O Remix 3 (v3.0.0), lançado em 1º de outubro de 2026, usa um modelo diferente da abordagem baseada em loaders do Remix 1/2, descrita nesta visão geral da arquitetura original do Remix baseada em loaders. Este artigo explica onde a busca de dados acontece agora, como o primeiro carregamento e as navegações fazem streaming, como funciona a UI de estado pendente e para onde vão os erros.

Principais conclusões

  • O Remix 3 busca os dados da página dentro de uma action de createController() para uma rota GET, e a action passa esses dados como props para o componente que renderiza com context.render().
  • As rotas são declaradas em app/routes.ts, implementadas por controllers e registradas com router.map(). Cada controller é responsável apenas pelas folhas diretas do seu mapa de rotas.
  • Um Frame sem fallback retém o HTML inicial até que sua rota responda. Um Frame com fallback envia esse fallback primeiro via streaming e o substitui depois.
  • Falhas esperadas, como registros inexistentes, devem ser retornadas como objetos Response com status 404 ou 400. Um erro lançado faz router.fetch() rejeitar, e o boundary do servidor retorna um 500.
  • Quando um frame adiado falha depois que seu fallback já foi enviado, o erro é reportado e o fallback permanece na tela.

O que substituiu o loader no Remix 3?

No Remix 3, uma action de controller faz o trabalho que antes era do loader de rota. O mapa de rotas declara uma URL e um método HTTP, e createController() associa uma action a cada folha de rota. A action lê valores tipados de context.params, aguarda seus dados e retorna um Response. O guia Routing and Controllers mostra esse padrão, e exports de loader não aparecem em nenhum lugar dele.

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

A principal mudança em relação ao Remix 2 é a direção do fluxo de dados. No Remix 2, o componente puxava seus dados por meio de um hook. No Remix 3, a action empurra os dados para o componente como props. context.render só existe quando o middleware render() está instalado no router. Sem ele, uma action ainda pode retornar um Response simples ou Response.json().

Onde fica o carregamento de dados no Remix 3?

Um app Remix 3 declara suas URLs e métodos HTTP em app/routes.ts, implementa uma action por folha de rota com createController() e registra cada controller com 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"

O helper href() de cada rota é tipado. Como :albumId é uma variável de caminho, albumId é um argumento obrigatório. Se você renomear o padrão mais tarde, qualquer chamada que ainda passe a chave antiga falhará na verificação 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);

Um controller é responsável apenas pelas folhas diretas do mapa que você passa para ele, então mapas aninhados precisam de sua própria chamada a router.map(). O Remix verifica cada mapeamento durante o setup: um controller sem action para uma de suas folhas faz o setup lançar um erro antes que qualquer requisição seja atendida. Na prática, planeje uma rota GET por página, mais uma para cada região que carrega de forma independente.

Como funcionam o primeiro carregamento e a navegação no Remix 3?

No primeiro carregamento, o middleware de render do Remix 3 faz o streaming de um único documento HTML. Na navegação, depois que run() é iniciado no navegador, links e formulários de mesma origem podem preencher novamente um único Frame nomeado, em vez de substituir o documento inteiro. Quando uma região precisa de sua própria requisição, a página a renderiza com Frame, apontando para a rota GET dessa região. A action por trás dessa rota retorna apenas o markup da região e deixa de fora o shell do documento do app.

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

Um Frame sem fallback é bloqueante: o Remix não envia o primeiro chunk de HTML até que a rota do frame tenha respondido, então a página chega completa. Adicione um fallback e ele passa a ser enviado no HTML inicial, sendo depois substituído pela saída da rota do frame quando essa action for concluída. Escolha o modo bloqueante quando a página não faz sentido sem a região, e um fallback quando vale a pena exibir o restante da página primeiro.

Depois que run() é iniciado no navegador, links e formulários de mesma origem podem preencher um frame nomeado em vez de navegar pelo documento inteiro. Esses atributos estão documentados em Streaming UI with Frames:

AtributoEfeito
data-rmx-targetSeleciona um frame nomeado montado
data-rmx-srcDefine a URL da requisição que preenche o frame
data-rmx-reset-scroll="false"Mantém a posição de rolagem atual
data-rmx-history="push|replace"Sobrescreve o comportamento padrão do histórico
data-rmx-documentExclui o link ou formulário da interceptação

Em um link, a barra de endereços ainda muda para o href, enquanto data-rmx-src decide o que é carregado no frame.

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

Os próprios componentes também podem recarregar frames. handle.frame.reload() recarrega o frame que contém o componente, e await handle.frames.get("album-recommendations")?.reload() tem como alvo um frame nomeado em outro ponto da página. Se nenhum frame com esse nome estiver montado, handle.frames.get() retorna undefined. As rotas de frame devem retornar apenas HTML confiável, portanto sanitize qualquer conteúdo não confiável no servidor antes.

Estados pendentes: fallbacks e cancelamento

O Remix 3 trata a UI de estado pendente com fallbacks de Frame e cancelamento de requisições. O fallback do Frame é a UI de carregamento inicial, e os guias não documentam um hook global de navegação pendente.

O cancelamento é algo que o próprio app precisa configurar. O resolver de frames do navegador que você passa para run() recebe um signal, e os guias orientam repassá-lo para o fetch(), de modo que a requisição de um frame seja interrompida quando o frame for removido ou quando uma requisição mais recente o substituir. No servidor, o middleware de render passa request.signal para renderToStream() e para suas requisições de frame. Para que o cancelamento chegue ao seu próprio código, passe request.signal também para chamadas ao banco de dados e de fetch. De acordo com Errors and Cancellation, quando uma requisição é abortada, o router rejeita com request.signal.reason.

Estados de erro: responses e hooks de reporte

No Remix 3, retorne um registro inexistente ou uma entrada inválida como um Response com status 404 ou 400. Um erro lançado faz router.fetch() rejeitar, e o boundary do servidor então responde com um 500. O Remix 3 não tem um error boundary de componente genérico; cada tipo de falha vai para o seu próprio destino:

FalhaPara onde vai
Esperada (registro inexistente, entrada inválida)Response retornado com status 404 ou 400
Erro lançado em uma action ou middlewarerouter.fetch() rejeita; o boundary do servidor reporta o erro e retorna um 500
Falha de renderização na raiz ou em um frame bloqueanteO erro é reportado e o stream rejeita
Falha em frame adiado após o envio do fallbackO erro é reportado; o fallback permanece na página
Runtime do navegador (hidratação, componente, scheduler, frame)Evento error no valor retornado por run()

No boundary do servidor, o createRequestListener de remix/node-fetch-server aceita uma opção onError. É nela que erros não capturados são registrados em log, e ela pode, opcionalmente, retornar um Response a ser enviado no lugar do 500 padrão. A opção onError do middleware de render e o evento error do navegador apenas reportam erros. Para frames, o próprio resolver do seu app decide no que uma resposta com falha se transforma: HTML delimitado para aquela região ou um erro enviado para esses hooks de reporte. O resolver dos guias lança um erro para qualquer status que não seja OK, então toda requisição de frame com falha é reportada.

Quando um frame adiado falha depois que seu fallback já foi enviado, o servidor não consegue trocar a página por uma resposta de erro, porque bytes do body já foram transmitidos. O erro é reportado e o fallback permanece na tela. Um log do servidor registra esse erro, mas não mostra o que o usuário viu. O session replay preenche essa lacuna: você pode ver usuários parados diante de uma mensagem “Carregando…” que nunca se resolveu enquanto continuavam usando o restante da página. Escreva fallbacks de forma que uma região travada ainda faça sentido caso nunca seja preenchida.

Migrando do Remix 2

Migrar um app Remix 2 para o Remix 3 é uma reescrita, não um upgrade. As rotas passam para um mapa de rotas tipado, o carregamento de dados passa para actions de controller e a UI passa para o runtime de componentes próprio do Remix em remix/component. Os guias não têm um capítulo de migração. O caminho de upgrade in-place do Remix 2 é o React Router v7, e o Remix 3 é uma base de código separada. Trate guias como este passo a passo de Next.js para Remix, escrito para o Remix 1/2, apenas como contexto, e não como um guia para o Remix 3.

Conclusão

O carregamento de dados no Remix 3 se resume a isto: uma rota GET, uma action de controller que aguarda os dados e retorna um Response, e frames para regiões que carregam ou falham de forma independente. Para começar, mapeie as rotas de uma página em app/routes.ts, mova a busca de dados dela para um controller e decida quais regiões precisam de um fallback. Para saber mais sobre como trabalhar com o novo framework, veja como o Remix 3 foi projetado para agentes de codificação com IA.

Perguntas frequentes

O Remix 3 usa React?

Não. O Remix 3 mantém a sintaxe JSX, mas não roda sobre o React nem sobre o React Router. Ele usa seu próprio modelo de componentes, exportado de remix/component. Um componente é composto por uma função externa de setup, que recebe um handle e é executada uma única vez, e por uma função interna de renderização, que é executada a cada atualização. Hooks do React, como useState, não fazem parte desse modelo, então bibliotecas de componentes React não funcionam no Remix 3 sem alterações.

Como funcionam envios de formulário e mutações no Remix 3?

Declare o formulário com o helper de rota form() em app/routes.ts. Por padrão, ele cria duas folhas na mesma URL: uma rota GET index, que exibe o formulário, e uma rota POST action, que trata o envio. As duas folhas pertencem ao mesmo controller. Após uma criação, atualização ou exclusão bem-sucedida, os guias do Remix 3 recomendam retornar um redirect 303 para que recarregar a página não reenvie o formulário.

Uma action de controller do Remix 3 pode retornar JSON em vez de HTML?

Sim. Uma action de controller pode retornar qualquer Response da Web, incluindo Response.json(), redirects, arquivos e streams. O guia Forms and Mutations do Remix 3 sugere JSON para casos como autocomplete, polling, widgets autocontidos, APIs e chamadas entre serviços. Ele desaconselha adicionar um caminho de mutação separado em JSON para um formulário que já funciona por meio de uma resposta HTML e um redirect.

O Remix 3 é a mesma coisa que o React Router v7?

Não. O Remix v2 foi incorporado ao React Router v7, lançado em novembro de 2024, como seu Framework Mode. Esse é o caminho de upgrade para apps Remix 2 que querem manter loaders e React. O Remix 3 é um projeto separado, anunciado em maio de 2025 e construído sobre seu próprio modelo de componentes. Migrar para ele significa reescrever rotas, carregamento de dados e 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.