Container Pattern — Arquitetura Frontend em Camadas
Padrão de organização para aplicações React que separa o que renderizar de como renderizar, usando 5 camadas com regras de dependência unidirecionais.
Questão central: como escalar sem quebrar?
As 5 Camadas
Types → contratos (interfaces puras, sem dependências)
↓
Infra → implementa os contratos (queries + mutations por domínio)
↓
Pages → conecta Infra com Components (lógica de wiring)
↓
Routes → containers finos (decide o que renderizar, boundaries)
↓
Components → puramente presentacionais (só recebem props)
Container vs Page — a distinção central
| Route (Container) | Page | |
|---|---|---|
| Responsabilidade | Decidir o que renderizar | Conectar dados com UI |
| Chama hooks de infra? | Não (só no beforeLoad) | Sim |
| Conhece a URL? | Sim (Route.useParams) | Não (recebe via props) |
| Tem Suspense/ErrorBoundary? | Sim | Não |
O container centraliza o que renderizar — as Pages. A page centraliza como renderizar — juntando infra com components.
Regras de dependência
| Camada | Pode importar de | Nunca importa de |
|---|---|---|
| Types | — | nada |
| Infra | Types | Components, Pages, Routes |
| Pages | Types, Infra, Components | Routes |
| Routes | Types, Infra (beforeLoad), Pages | — |
| Components | Types (via props) | Infra, Pages, Routes |
Estrutura de pastas
Cada domínio repete a mesma estrutura nas 5 camadas:
src/
├── types/
│ ├── order.ts # Order, CreateOrderPayload
│ └── components/
│ └── order.ts # OrderTableProps, OrderFormProps
│
├── infra/orders/
│ ├── queries.ts # useOrdersQuery, useOrderByIdQuery
│ └── mutations.ts # useCreateOrderMutation
│
├── components/features/orders/ # OrderTable, OrderForm (presentacionais)
│
├── pages/orders/
│ └── OrdersPage.tsx # wiring: useOrdersQuery → <OrderTable />
│
└── routes/_private/orders/
└── index.tsx # container: Suspense + ErrorBoundary + <OrdersPage />
Adicionar um domínio novo não altera nenhum arquivo existente.
Fluxo de dados
URL: /characters?search=mario&page=2
│
▼
Route (container)
├── ErrorBoundary
├── Suspense fallback={<Loading />}
└── <CharactersPage />
│
▼
Page (wiring)
├── useQueryStates() ← lê search params
├── useCharactersQuery(params) ← GET /characters?search=mario&page=2
├── useDeleteCharacterMutation() ← DELETE /characters/:id
└── <CharacterList ← Component (presentacional)
characters={data.items}
onDelete={deleteMutate}
/>
Quem sabe o quê
| Conhecimento | Camada |
|---|---|
| Qual URL a rota responde | Route |
| Se o usuário precisa estar autenticado | Route (beforeLoad) |
| Que o dado vem de uma API e fica em cache | Infra |
| Que o botão "Excluir" chama a API | Page |
| Como renderizar uma lista de cards | Component |