Passo a semana alternando entre os dois. Sistemas administrativos, com muita tela de cadastro, tabela com filtro e formulário, saem em Angular. Produtos que precisam de SEO, páginas públicas e este site saem em Next.js. Os dois resolvem o mesmo problema, montar interface com TypeScript, mas partem de premissas diferentes. O que mais custa na troca é levar a premissa errada de um para o outro.
1. Um framework completo e um framework sobre o React
O Angular vem com quase tudo dentro: componentes, injeção de dependência, roteador, formulários, cliente HTTP, ferramentas de teste e uma CLI que gera e atualiza o projeto [1]. O Next.js é um framework em cima do React. Ele traz roteamento, renderização no servidor, cache e Server Functions, e deixa o resto para você escolher [2].
Na prática, um projeto Angular novo se parece com qualquer outro projeto Angular. Em Next.js, dois projetos podem ter escolhas completamente diferentes de estado, formulário e busca de dados. Isso dá liberdade, mas cobra combinado: no Next, as convenções do time precisam estar escritas, porque o framework não as impõe.
2. Componentização
Os dois são orientados a componentes, mas o que é um componente muda bastante. No Angular, um componente é uma classe com um decorador, um template HTML e estilos próprios. Ele recebe dados por input() [12], avisa o pai por output() [13] e recebe conteúdo de fora pelo <ng-content> [14]. Os estilos ficam presos ao componente por padrão: o CSS dele não vaza para o resto da página [15].
@Component({
selector: 'app-card',
templateUrl: './card.html',
styleUrl: './card.css',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class Card {
readonly titulo = input.required<string>()
readonly destaque = input(false)
readonly fechar = output<void>()
}<article class="card" [class.destaque]="destaque()">
<header>
<h2>{{ titulo() }}</h2>
<button type="button" (click)="fechar.emit()">Fechar</button>
</header>
<ng-content />
</article><app-card titulo="Resumo" [destaque]="true" (fechar)="ocultar()">
<p>Este conteúdo é projetado dentro do card.</p>
</app-card>No React, um componente é uma função que recebe props e devolve JSX. Não tem decorador nem arquivo de template separado, evento é só uma função passada como prop, e o conteúdo de dentro chega pela prop children [16]:
import type { ReactNode } from 'react'
type CardProps = {
titulo: string
destaque?: boolean
onFechar: () => void
children: ReactNode
}
export function Card({ titulo, destaque = false, onFechar, children }: CardProps) {
return (
<article className={destaque ? 'card card--destaque' : 'card'}>
<header>
<h2>{titulo}</h2>
<button type="button" onClick={onFechar}>Fechar</button>
</header>
{children}
</article>
)
}<Card titulo="Resumo" destaque onFechar={ocultar}>
<p>Este conteúdo chega como children.</p>
</Card>A diferença de verdade aparece no resto. No Angular, cada tipo de reuso tem o seu bloco: componente para interface, diretiva para comportamento num elemento, pipe para formatar valor e serviço para lógica e estado compartilhado. No React, é tudo função: lógica reaproveitável vira um hook, variação de interface vira prop ou composição, e o estilo é escolha sua, seja CSS Modules, Tailwind ou CSS puro.
Por isso o React, e o Next.js em cima dele, é bem mais customizável. Dá para montar a arquitetura do jeito que o projeto pede, com as bibliotecas que o time preferir. O Angular troca essa liberdade por padrão: menos decisões para tomar e um projeto que todo mundo do time lê do mesmo jeito. Nenhum dos dois é melhor por isso. São apostas diferentes.
3. Onde o código roda
Esta é a maior diferença, e a que mais confunde. Uma aplicação Angular é, por padrão, uma SPA: o navegador baixa o JavaScript e tudo roda nele. O SSR existe e funciona bem, mas é uma escolha que você adiciona com ng add @angular/ssr [3].
No App Router do Next.js é o contrário: páginas e layouts são Server Components por padrão [2]. Eles rodam no servidor, podem acessar banco e segredos e não mandam o próprio código para o navegador. Só vira código de navegador o que você marca com 'use client' [4].
import { BotaoFavoritar } from './botao-favoritar'
import { buscarProduto } from '@/lib/produtos'
// Server Component: roda no servidor e pode ser async.
export default async function Page({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const produto = await buscarProduto(id)
return (
<main>
<h1>{produto.nome}</h1>
<BotaoFavoritar inicial={produto.favorito} />
</main>
)
}'use client'
import { useState } from 'react'
// Client Component: tem estado e evento, então vai para o navegador.
export function BotaoFavoritar({ inicial }: { inicial: boolean }) {
const [favorito, setFavorito] = useState(inicial)
return (
<button onClick={() => setFavorito(!favorito)}>
{favorito ? 'Favoritado' : 'Favoritar'}
</button>
)
}A regra que eu sigo: começo tudo como Server Component e só levo para o cliente o pedaço que precisa de estado, evento ou API do navegador. Quanto menor a ilha de 'use client', menos JavaScript a página manda [2]. Um detalhe que pega quem vem de versões antigas: desde o Next.js 15, params é uma Promise e precisa de await.
4. Buscar dados
No Angular, a busca de dados mora num serviço injetável, com o HttpClient devolvendo um Observable. O componente pede o serviço com inject() e transforma o resultado em signal para usar no template:
@Injectable({ providedIn: 'root' })
export class ProdutosApi {
private readonly http = inject(HttpClient)
listar(): Observable<Produto[]> {
return this.http.get<Produto[]>('/api/produtos')
}
}@Component({
selector: 'app-produtos',
templateUrl: './produtos.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ProdutosPage {
private readonly api = inject(ProdutosApi)
protected readonly produtos = toSignal(this.api.listar(), { initialValue: [] })
protected readonly busca = signal('')
protected readonly filtrados = computed(() =>
this.produtos().filter((p) =>
p.nome.toLowerCase().includes(this.busca().toLowerCase()),
),
)
}No Next.js, num Server Component, buscar dado é só await. Não tem useEffect nem estado de carregamento escrito à mão: o componente é uma função assíncrona e o Next cuida do streaming [5]. Separar a busca numa camada própria continua sendo boa prática, mas é uma decisão sua, não do framework.
export default async function Page() {
const produtos = await buscarProdutos()
return (
<ul>
{produtos.map((p) => (
<li key={p.id}>{p.nome}</li>
))}
</ul>
)
}5. Reatividade: signals e re-render
O Angular moderno é construído em cima de signals. Um signal é um valor que avisa quem depende dele quando muda, e um computed deriva de outros signals: só calcula quando é lido e guarda o resultado até uma dependência mudar [6]. No Angular 21, projetos novos já nem incluem o zone.js; a atualização da tela é guiada pelos signals [7].
No React, a unidade é o componente: quando o estado muda, a função do componente roda de novo e o React compara o resultado. O mesmo filtro do exemplo acima fica assim:
'use client'
import { useMemo, useState } from 'react'
export function ListaProdutos({ produtos }: { produtos: Produto[] }) {
const [busca, setBusca] = useState('')
const filtrados = useMemo(
() =>
produtos.filter((p) =>
p.nome.toLowerCase().includes(busca.toLowerCase()),
),
[produtos, busca],
)
// ...
}O que muda na cabeça: no Angular eu penso em quais valores dependem de quais; no React, em quando o componente renderiza e no que precisa ser memorizado. Esquecer uma dependência no array do useMemo é o erro clássico de quem vem do Angular. Ler um signal fora de um contexto reativo e esperar que ele se atualize é o erro clássico de quem faz o caminho inverso.
6. Rotas
No Angular, as rotas são um array de configuração, com guard e lazy loading declarados por rota [8]:
export const routes: Routes = [
{
path: 'produtos',
canActivate: [autenticadoGuard],
loadChildren: () =>
import('./produtos/produtos.routes').then((m) => m.PRODUTOS_ROUTES),
},
{
path: '**',
loadComponent: () =>
import('./erro/nao-encontrada').then((m) => m.NaoEncontradaPage),
},
]No Next.js, a rota é a pasta. app/produtos/[id]/page.tsx já é a rota /produtos/:id, e arquivos com nome reservado, como layout.tsx e loading.tsx, cuidam da moldura e do estado de carregamento:
app/
├── layout.tsx → moldura de todas as páginas
├── page.tsx → /
└── produtos/
├── page.tsx → /produtos
└── [id]/
├── page.tsx → /produtos/:id
└── loading.tsx → estado de carregando da rotaNão existe um equivalente direto do guard. A checagem de acesso acontece no servidor, na página, no layout ou na Server Function, e redirecionamentos gerais ficam no proxy, que é o antigo middleware renomeado no Next.js 16 [9].
7. Formulários
Formulário é onde o Angular mais brilha nos sistemas que eu faço. Os Reactive Forms vêm no framework, com validação, estado de campo tocado e alterado, e tipagem [10]:
export class NovoProdutoPage {
private readonly fb = inject(NonNullableFormBuilder)
protected readonly form = this.fb.group({
nome: ['', [Validators.required, Validators.maxLength(120)]],
preco: [0, [Validators.required, Validators.min(0)]],
})
}No Next.js, o caminho nativo é o formulário HTML chamando uma Server Function pelo action. O dado vai direto para o servidor e chega como FormData [11]. Para validação mais rica no cliente, entra uma biblioteca de sua escolha.
'use server'
export async function criarProduto(formData: FormData) {
// Toda Server Function é um endpoint público:
// valide e cheque permissão aqui, mesmo que o formulário já valide.
const nome = String(formData.get('nome') ?? '').trim()
if (!nome) throw new Error('Nome é obrigatório')
// salvar, revalidar o cache…
}<form action={criarProduto}>
<input name="nome" required />
<button type="submit">Salvar</button>
</form>O que eu recomendo
Esta parte é opinião minha, depois de usar os dois em produção:
- Para sistema interno grande, eu recomendo Angular. Muitas telas de cadastro, muito formulário, time grande ou rotativo: a estrutura imposta faz um projeto se parecer com o outro, e quem entra no time acha as coisas onde espera. Só os Reactive Forms já pagam a escolha.
- Para produto, página pública e tudo que depende de SEO, eu recomendo Next.js. Renderizar no servidor por padrão deixa a página rápida sem esforço extra, e a liberdade do React permite montar exatamente a interface que o produto pede.
- Se o time quer flexibilidade, Next.js; se precisa de padrão, Angular. A customização do React é ótima quando alguém cuida das convenções. Sem isso, cada parte do projeto acaba com um estilo diferente.
- Na dúvida, escolha o que o time já domina. Os dois atendem bem a maioria dos casos, e a curva de aprendizado custa mais do que qualquer diferença de recurso.
O que levo de um para o outro
Do Angular para o Next, levo a separação da busca de dados em funções próprias, mesmo sem o framework exigir, e a tipagem forte nas bordas. Do Next para o Angular, levo a pergunta "isso precisa mesmo rodar no navegador?" e a disposição de ligar o SSR quando a página é pública.
E dos dois, uma tendência: signals de um lado e Server Components do outro estão empurrando os frameworks na mesma direção, com menos trabalho escondido e mais clareza sobre o que reage a quê e onde cada coisa roda. Quem entende essas duas ideias troca de um para o outro sem sofrer.
Fontes
- What is Angular? — Documentação do Angular
- Server and Client Components — Documentação do Next.js
- Server-side and hybrid rendering — Documentação do Angular
- Server Components — Documentação do React
- Fetching Data — Documentação do Next.js
- Signals — Documentação do Angular
- What’s new in Angular 21.0? — Ninja Squad, novembro de 2025
- Lazy loading routes — Documentação do Angular
- Proxy — Documentação do Next.js
- Reactive forms — Documentação do Angular
- Mutating Data — Documentação do Next.js
- Accepting data with input properties — Documentação do Angular
- Custom events with outputs — Documentação do Angular
- Content projection with ng-content — Documentação do Angular
- Styling components — Documentação do Angular
- Passing Props to a Component — Documentação do React