6

Clean Architecture no Frontend React: Separando Concerns de Verdade

O problema real: regra de negócio morando dentro de componente

Abra qualquer projeto React com mais de 20 telas. Procure um useEffect que faz fetch, transforma dados, aplica regra de negócio e seta estado no mesmo bloco. Esse padrão aparece em quase todo codebase que cresceu sem separação de camadas.

O resultado é previsível: testar a lógica de cálculo de desconto exige renderizar componente, mockar hook de roteamento e simular evento de clique. Uma regra que deveria ser uma função pura de 10 linhas vira refém do React.

Clean Architecture no frontend não significa replicar a estrutura de um backend Java com 47 pastas. Significa que a regra de negócio roda sem importar React, que trocar Axios por fetch nativo não exige mexer em 30 arquivos, e que um teste unitário de domínio executa em milissegundos sem jsdom.

Se você já leu o post sobre Clean Architecture com TypeScript e Node.js, a ideia central é a mesma: dependências apontam para dentro, nunca para fora. A diferença está em como adaptar isso para um ambiente onde o framework (React) é onipresente.

As camadas e o que vai em cada uma

A divisão que funciona para projetos React de médio porte (10-50 telas, 3+ devs) usa quatro camadas:

CamadaResponsabilidadeDepende deExemplo concreto
DomainEntidades, value objects, regras purasNadacalculateOrderTotal, OrderStatus
ApplicationUse cases, orquestração de regrasDomainCreateOrderUseCase, ApplyDiscountUseCase
InfrastructureHTTP clients, storage, APIs externasApplication (via interface)HttpOrderRepository, LocalStorageCartGateway
PresentationComponentes React, hooks, páginasApplication (via interface)useCreateOrder, OrderPage

A regra de ouro: Domain não importa nada de fora. Application importa Domain. Infrastructure e Presentation implementam interfaces definidas em Application.

Domain: lógica que não sabe que React existe

// src/domain/entities/Order.ts

export type OrderStatus = "draft" | "confirmed" | "shipped" | "delivered";

export interface OrderItem {
  productId: string;
  name: string;
  unitPrice: number;
  quantity: number;
}

export interface Order {
  id: string;
  items: OrderItem[];
  status: OrderStatus;
  couponCode: string | null;
  createdAt: Date;
}

// Regra de negócio pura: desconto por cupom aplicado ao total
export function calculateOrderTotal(
  items: OrderItem[],
  discountPercent: number
): number {
  const subtotal = items.reduce(
    (sum, item) => sum + item.unitPrice * item.quantity,
    0
  );

  // Desconto nunca ultrapassa 30%, independente do cupom
  const clampedDiscount = Math.min(discountPercent, 30);

  return subtotal * (1 - clampedDiscount / 100);
}

export function canCancelOrder(order: Order): boolean {
  // Pedido só pode ser cancelado antes do envio
  return order.status === "draft" || order.status === "confirmed";
}

Esse arquivo não importa React, Axios, Zustand nem qualquer dependência externa. Testar calculateOrderTotal é chamar a função com argumentos e comparar o retorno. Sem render, sem provider, sem mock.

Application: use cases com contratos explícitos

A camada Application define o que o sistema faz sem dizer como. Ela declara interfaces (ports) que Infrastructure vai implementar.

// src/application/ports/OrderRepository.ts

import type { Order } from "@/domain/entities/Order";

// Port: contrato que a infraestrutura deve cumprir
export interface OrderRepository {
  findById(id: string): Promise<Order | null>;
  save(order: Order): Promise<void>;
  findByStatus(status: string): Promise<Order[]>;
}
// src/application/usecases/CreateOrderUseCase.ts

import type { Order, OrderItem } from "@/domain/entities/Order";
import type { OrderRepository } from "@/application/ports/OrderRepository";
import { calculateOrderTotal } from "@/domain/ent

---

Leia o artigo completo em [https://www.vivodecodigo.com.br/react/clean-architecture-frontend-react-separando-concerns](https://www.vivodecodigo.com.br/react/clean-architecture-frontend-react-separando-concerns)
Carregando publicação patrocinada...