5 Coisas Para as Quais Você Não Precisa do React
Cinco APIs nativas do navegador substituem componentes React comuns: dialog, Popover, Custom Elements, container queries e View Transitions.
A plataforma do navegador passou a oferecer substitutos nativos e amplamente disponíveis para vários primitivos de UI que antes exigiam componentes React ou bibliotecas de terceiros: diálogos modais, popovers e tooltips, widgets reutilizáveis independentes de framework, layouts responsivos com consciência de contêiner e transições animadas entre views. Este artigo não é um argumento contra o React — ele continua sendo a ferramenta certa para estado compartilhado complexo, fluxos de trabalho com formulários extensos e ecossistemas como Next.js e Remix. Trata-se de uma lista de verificação para mantenedores: cinco categorias de componentes que você pode já ter em uma base de código React e que o navegador agora consegue lidar nativamente, sem nenhum peso adicional no bundle JavaScript.
O público-alvo aqui é o desenvolvedor React em atividade cujo modelo mental sobre “o que o navegador consegue fazer” parou de ser atualizado por volta de 2020. O React 19 é a versão estável atual, e vários dos padrões que seu ecossistema resolveu agora fazem parte da plataforma. Cada seção abaixo identifica o reflexo React, a API nativa que o substitui e a ressalva específica de acessibilidade que você precisa conhecer antes de deletar código.
Principais Conclusões
- O elemento HTML
<dialog>comshowModal()fornece aprisionamento de foco nativo, descarte com a tecla Escape e um pseudo-elemento::backdrop— eliminando a maioria das razões para depender de uma biblioteca modal React. - A Popover API renderiza elementos na camada superior do navegador, o que remove toda a classe de bugs de z-index e recorte por
overflow: hiddenque assolam tooltips e dropdowns React construídos manualmente. - Custom Elements com Shadow DOM permitem distribuir um único widget que funciona em qualquer framework ou em HTML puro, sem precisar reimplementá-lo por stack.
- CSS Container Queries (
@container) permitem que um componente responda à largura de seu próprio pai, substituindo hooksResizeObservere estado React utilizados exclusivamente para decisões de layout. - A View Transitions API (
document.startViewTransition()) anima mudanças de estado no DOM nativamente, cobrindo muitos casos de uso anteriormente tratados pelo Framer Motion ou peloreact-transition-group.
Modais: use o elemento <dialog> em vez de uma biblioteca modal
Para diálogos modais, o elemento HTML <dialog> nativo, invocado via showModal(), oferece aprisionamento de foco, conteúdo de fundo inerte, descarte com a tecla Escape e estilização do backdrop — comportamentos que um modal React personalizado precisa implementar manualmente e frequentemente implementa de forma incorreta. O elemento <dialog> faz parte do Baseline; verifique a data exata de disponibilidade no MDN antes de publicar documentação interna.
O padrão React. As equipes geralmente recorrem ao react-modal, ao Radix Dialog ou a um hook useModal personalizado apoiado em um portal. O padrão com hook normalmente combina createPortal, um useEffect que alterna document.body.style.overflow e um focus trap escrito à mão. Gravações de sessão em produção dessas implementações frequentemente revelam usuários navegando com Tab para fora do modal e chegando ao conteúdo de fundo — um sintoma de lógica de focus trap incompleta.
A API nativa.
<dialog id="confirm" aria-labelledby="confirm-title">
<h2 id="confirm-title">Excluir projeto?</h2>
<p>Esta ação não pode ser desfeita.</p>
<form method="dialog">
<button value="cancel">Cancelar</button>
<button value="confirm">Excluir</button>
</form>
</dialog>
<script>
document.getElementById('confirm').showModal();
</script>
showModal() coloca o diálogo na camada superior, aprisiona o foco dentro dele, torna o restante do documento inerte e renderiza o pseudo-elemento ::backdrop, que pode ser estilizado com CSS. Um <form method="dialog"> fecha o diálogo e retorna o value do botão clicado via dialog.returnValue — sem necessidade de event listener.
Ressalvas. O problema de acessibilidade é que o <dialog> não anuncia um rótulo automaticamente. Você precisa de aria-labelledby apontando para um cabeçalho visível (ou aria-label) para que leitores de tela consigam identificá-lo. Se o diálogo for não-modal — aberto com show() em vez de showModal() — ele não aprisiona o foco, e você pode preferir a Popover API. React ou uma biblioteca continua sendo a melhor escolha quando você precisa de lógica declarativa de abertura/fechamento vinculada a estado e fortemente acoplada a outros componentes, ou de animação que seja executada antes que o diálogo seja desmontado.
Popovers, tooltips e dropdowns: use a Popover API
A Popover API renderiza elementos na camada superior do navegador, o que significa que um popover sempre aparece acima de outros conteúdos, independentemente do contexto de empilhamento ou de overflow: hidden em um ancestral. Isso elimina toda a categoria de conflitos de z-index e bugs de recorte que implementações manuais de tooltip e dropdown produzem.
O padrão React. Floating UI, Radix Popover e os primitivos de overlay do React-Aria são dependências comuns. Eles lidam com posicionamento, descarte ao clicar fora e renderização via portal. Para um tooltip simples, isso representa muito código a ser importado.
A API nativa.
<button popovertarget="menu">Abrir menu</button>
<div id="menu" popover>
<a href="/account">Conta</a>
<a href="/logout">Sair</a>
</div>
O atributo popover sozinho — sem nenhum JavaScript — fornece um elemento que alterna via um botão popovertarget, é descartado ao clicar fora e com Escape, e é renderizado na camada superior. O valor padrão popover="auto" habilita o descarte por clique externo (light-dismiss); popover="manual" o desabilita para casos em que você deseja controle explícito. A Popover API é Baseline Newly Available; verifique a tabela de compatibilidade do MDN para o status atual.
Ressalvas. O problema de acessibilidade é que, ao contrário do showModal() do <dialog>, a Popover API não gerencia o foco automaticamente. Se o seu popover funcionar como um menu, você ainda precisa aplicar role="menu", gerenciar o roving tabindex e mover o foco para dentro do popover quando ele abrir. Para posicionamento relativo ao elemento que o aciona, você também precisa do CSS Anchor Positioning, cujo status no Baseline é mais limitado — verifique no MDN antes de utilizá-lo em múltiplos navegadores. Para menus complexos com submenus, padrões de navegação por teclado e typeahead, uma biblioteca como Radix ou React-Aria ainda economiza trabalho real.
Discover how at OpenReplay.com.
Widgets reutilizáveis: use Custom Elements e Shadow DOM
Um Custom Element registrado com customElements.define() funciona em qualquer contexto HTML — React, Vue, Angular, Svelte ou um arquivo HTML puro — sem necessidade de reimplementação. Combinado com Shadow DOM, ele fornece encapsulamento de estilos sem CSS Modules, CSS-in-JS ou uma etapa de build. Custom Elements e Shadow DOM são Baseline Widely Available; verifique o ano no MDN.
Web Components não substituíram o React no desenvolvimento de aplicações mainstream. O que eles substituíram foi a necessidade de distribuir o mesmo widget cinco vezes — uma por framework — quando você mantém um design system ou distribui um embed de terceiros.
O padrão React. Um botão, badge ou gráfico reutilizável encapsulado em um componente React, publicado no npm e reimplementado (ou re-encapsulado) para qualquer equipe que use um framework diferente.
A API nativa.
class CopyButton extends HTMLElement {
connectedCallback() {
this.attachShadow({ mode: 'open' }).innerHTML = `
<style>button { padding: 6px 12px; }</style>
<button><slot>Copiar</slot></button>
`;
this.shadowRoot.querySelector('button')
.addEventListener('click', () => {
navigator.clipboard.writeText(this.dataset.value ?? '');
});
}
}
customElements.define('copy-button', CopyButton);
Utilizado como <copy-button data-value="hello">Copiar</copy-button> em qualquer HTML, inclusive dentro de JSX. O React 19 suporta custom elements diretamente, incluindo a passagem de props como objetos e a escuta de eventos customizados.
Ressalvas. O problema de acessibilidade é que a árvore de acessibilidade não atravessa os limites do shadow DOM por padrão — referências de aria-labelledby e aria-describedby no light DOM não conseguem apontar para IDs dentro de um shadow root, e vice-versa. A especificação ARIA in HTML e a proposta reference target em andamento abordam isso, mas os padrões práticos hoje exigem ou atributos ARIA explícitos no elemento host ou attachInternals() com ElementInternals. React ainda é a melhor escolha quando um widget precisa se integrar estreitamente com o estado da aplicação, compartilhar React Context ou usar Suspense.
Layout responsivo em nível de componente: use CSS Container Queries
CSS Container Queries (@container) permitem que um componente adapte seu layout com base na largura de seu próprio pai, em vez da viewport. Isso elimina o padrão de hook useResizeObserver, no qual o estado React rastreia as dimensões do contêiner exclusivamente para controlar um className. Container Queries são Baseline Widely Available — verifique o ano no MDN.
O padrão React. Um hook useResizeObserver (frequentemente do @react-hook/resize-observer ou escrito à mão) conectado ao estado do componente, que alterna uma prop layout="compact" ou um className. Cada redimensionamento aciona uma renderização React, mesmo que o único consumidor seja o CSS.
A API nativa.
.card-container {
container-type: inline-size;
}
.card {
display: grid;
grid-template-columns: 1fr;
}
@container (min-width: 400px) {
.card {
grid-template-columns: 120px 1fr;
}
}
Declare container-type: inline-size no pai e escreva regras @container para o filho. O navegador lida com a observação de redimensionamento nativamente. Sem JavaScript, sem re-renderizações, sem incompatibilidade de hidratação.
O seletor :has() complementa isso para estilização baseada em estado. Uma regra como form:has(input:invalid) button[type="submit"] { opacity: 0.5 } expressa o que antes exigia useState e um padrão de input controlado. :has() é Baseline Widely Available — verifique no MDN.
Ressalvas. A consideração de acessibilidade é sutil, mas real: container queries podem alterar o layout drasticamente sem modificar a ordem do DOM, o que é positivo para leitores de tela, mas significa que você ainda deve verificar se a ordem de leitura corresponde à ordem visual em cada breakpoint. Container queries também introduzem comportamento de contenção que pode afetar como elementos descendentes são dispostos e posicionados, portanto, teste componentes que dependem de posicionamento relativo à viewport ou de outras premissas de layout. O estado React ainda é necessário quando a decisão de layout vai além da estilização — por exemplo, quando você precisa renderizar uma árvore de componentes diferente, e não apenas reestilizar uma existente.
Transições animadas: use a View Transitions API
A View Transitions API envolve uma atualização do DOM em uma animação de cross-fade por padrão, com controle total via CSS sobre a transição usando pseudo-elementos ::view-transition-*. Para transições no mesmo documento, ela cobre a maioria das animações de transição de rota e de estado que antes exigiam bibliotecas de animação.
O padrão React. Framer Motion, react-transition-group ou wrappers AnimatePresence em torno de componentes de rota. Eles funcionam, mas exigem que a animação seja expressável no modelo de renderização do React, o que é complicado para transições que abrangem a desmontagem de uma árvore e a montagem de outra.
A API nativa.
function navigate(url) {
if (!document.startViewTransition) {
updateDOM(url);
return;
}
document.startViewTransition(() => updateDOM(url));
}
document.startViewTransition() recebe um callback que realiza a atualização do DOM. O navegador captura o estado anterior, executa o callback, captura o estado posterior e faz o cross-fade entre eles. Para animar um elemento específico durante a transição — por exemplo, uma miniatura se expandindo para uma visualização de detalhe — atribua o mesmo view-transition-name nos elementos correspondentes via CSS. As View Transitions no mesmo documento são Baseline Newly Available; as View Transitions entre documentos (para navegações MPA) têm suporte mais limitado — verifique a tabela de compatibilidade do MDN e o blog do WebKit para o status atual do Safari antes de depender do modo entre documentos.
Ressalvas. O problema de acessibilidade é o movimento: respeite prefers-reduced-motion envolvendo a transição em uma media query ou ignorando a chamada completamente para usuários que optam por não receber animações. O cross-fade padrão é breve, mas ainda é uma animação. As bibliotecas React continuam sendo a melhor escolha quando você precisa de física de mola (spring physics), transições controladas por gestos ou animações que interrompem e revertem no meio da execução — as view transitions são atômicas e não foram projetadas para isso.
Onde o React ainda vence
As cinco substituições acima têm como alvo categorias específicas de componentes. Para tudo o que está listado abaixo, o React ainda é a ferramenta certa, e substituí-lo por recursos da plataforma custaria mais do que economizaria.
- Estado compartilhado complexo entre componentes distantes. Quando múltiplas partes não relacionadas da UI assinam o mesmo estado em evolução com seletores derivados, bibliotecas como Zustand, Jotai ou Redux Toolkit realizam trabalho que a plataforma não faz. Eventos customizados em Web Components podem transportar dados, mas não modelam estado derivado.
- Fluxos de trabalho com formulários extensos, validação entre campos e renderização dinâmica. O
<form>nativo, a Constraint Validation API e oFormDatalidam bem com o envio de formulários simples. Wizards de múltiplas etapas, campos condicionais que dependem de valores em outras partes do formulário, validação orientada pelo servidor mesclada com validação do cliente e arrays de campos ainda se beneficiam do React Hook Form ou do TanStack Form. - Renderização orientada pelo servidor e busca de dados. React Server Components, o hook
use()para dados assíncronos e o modelo de SSR com streaming no Next.js e no Remix resolvem problemas de hidratação, code-splitting e coordenação de busca de dados que a plataforma não aborda diretamente. - Maturidade do ecossistema para roteamento e camadas de dados. TanStack Router, TanStack Query e o ecossistema consolidado do React Router fornecem invalidação de cache, atualizações otimistas e padrões de route-loader que exigiriam um trabalho significativo para replicar com APIs nativas.
- Convenções de equipe e investimento existente. Uma base de código, pipeline de contratação, design system e CI construídos em torno do React são, por si só, um ativo. A postura de auditoria aqui é remover componentes específicos onde a plataforma agora é suficiente — não migrar stacks.
A ação prática: abra o maior diretório de componentes React do seu projeto, pesquise por Modal, Popover, Tooltip, Dropdown e qualquer importação de useResizeObserver. Cada um é um candidato à substituição nativa descrita acima. Verifique o status do Baseline da API no MDN para o intervalo de navegadores que você suporta, distribua a substituição por trás de uma feature flag e meça o delta do bundle. O navegador se atualizou — o trabalho restante é auditar quais dependências você não precisa mais.
Perguntas Frequentes
Posso usar o elemento dialog nativo com o modelo de estado do React?
Sim. Anexe uma ref ao elemento dialog e chame ref.current.showModal() ou ref.current.close() a partir de effects controlados pelo estado React. O dialog permanece na árvore React e aceita filhos JSX normalmente, mas você contorna as props de abertura controladas por useState na saída renderizada. A principal dificuldade é que o React não re-executa effects para o evento cancel interno do dialog, portanto, anexe um listener nativo de close via useEffect para sincronizar o estado de volta.
Como Custom Elements passam dados complexos para o React, e vice-versa?
O React 19 passa props que não são strings diretamente para as propriedades do custom element em vez de serializá-las para atributos, portanto objetos e arrays funcionam sem codificação JSON. Custom elements retornam dados via CustomEvent, que o React 19 escuta usando props de handler com o prefixo on (por exemplo, onMyEvent). No React 18 e versões anteriores, você deve anexar event listeners imperativamente via ref, pois os eventos sintéticos não lidam com nomes de eventos customizados.
Container queries e o seletor :has() prejudicam o desempenho de renderização?
Ambos têm um custo mensurável, mas geralmente são mais baratos do que as alternativas JavaScript que substituem. Container queries exigem que o navegador mantenha um contexto de contenção e reavalie as regras correspondentes nas mudanças de tamanho, o que ainda é mais rápido do que um callback ResizeObserver acionando uma renderização React. O seletor :has() pode ser custoso quando usado com seletores de sujeito amplos em árvores DOM grandes; restrinja-o a pais específicos em vez de aplicá-lo a elementos body ou de nível raiz.
A View Transitions API funciona com roteadores client-side como o React Router?
Sim, para transições no mesmo documento. Envolva o callback de navegação do seu roteador em document.startViewTransition() para que a atualização do DOM que o React realiza durante a mudança de rota seja executada dentro da transição. O React Router v6 e o TanStack Router suportam esse padrão por meio de interceptação de navegação. As view transitions entre documentos, que animam carregamentos completos de página, exigem opt-in adicional via a regra CSS @view-transition e têm suporte mais restrito nos navegadores — verifique no MDN antes de depender delas.