12k
All articles

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.

OpenReplay Team
OpenReplay Team
5 Coisas Para as Quais Você Não Precisa do React

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> com showModal() 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: hidden que 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 hooks ResizeObserver e 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 pelo react-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.

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 o FormData lidam 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.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.