typescriptfrontendreact

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
ResponsabilidadeDecidir o que renderizarConectar 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?SimNão

O container centraliza o que renderizar — as Pages. A page centraliza como renderizar — juntando infra com components.

Regras de dependência

CamadaPode importar deNunca importa de
Typesnada
InfraTypesComponents, Pages, Routes
PagesTypes, Infra, ComponentsRoutes
RoutesTypes, Infra (beforeLoad), Pages
ComponentsTypes (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ê

ConhecimentoCamada
Qual URL a rota respondeRoute
Se o usuário precisa estar autenticadoRoute (beforeLoad)
Que o dado vem de uma API e fica em cacheInfra
Que o botão "Excluir" chama a APIPage
Como renderizar uma lista de cardsComponent