React 19 useEffectEvent e Activity: novas APIs e perguntas de entrevista 2026

Analise aprofundada das APIs useEffectEvent e Activity do React 19.2. Solucoes para stale closures, pre-renderizacao em segundo plano, exemplos de codigo e perguntas de entrevista tecnica.

Diagrama da arquitetura das APIs useEffectEvent e Activity do React 19.2

O React 19.2 trouxe duas APIs que resolvem problemas antigos e recorrentes no desenvolvimento de aplicacoes React: o useEffectEvent elimina stale closures dentro de efeitos, e o componente <Activity> permite pre-renderizacao em segundo plano com preservacao de estado. Ambas foram lancadas em outubro de 2025 e ja estao transformando a forma como aplicacoes React em producao lidam com efeitos colaterais e navegacao entre rotas.

Requisito minimo: React 19.2

useEffectEvent e Activity exigem React 19.2 ou superior. A atualizacao pode ser feita com npm install react@latest react-dom@latest. O plugin de ESLint eslint-plugin-react-hooks@6+ adiciona suporte nativo para useEffectEvent nos arrays de dependencias.

O problema das stale closures em efeitos

Todo desenvolvedor React ja enfrentou o problema de stale closures. Um efeito captura um valor no momento da renderizacao e, quando esse valor muda, o efeito continua referenciando a versao antiga. A solucao classica envolvia o uso de useRef para manter uma referencia mutavel -- funcional, porem verbosa e invisivel para o linter.

O exemplo a seguir ilustra uma aplicacao de chat que registra analytics ao receber uma nova mensagem. O log precisa incluir o theme atual, mas mudancas no theme nao devem forcar uma reconexao do chat:

hooks/useChatRoom.tstsx
// Before useEffectEvent: useRef workaround
import { useEffect, useRef } from 'react'

export function useChatRoom(roomId: string, theme: string) {
  // Store theme in a ref to avoid stale closure
  const themeRef = useRef(theme)
  themeRef.current = theme

  useEffect(() => {
    const connection = createConnection(roomId)
    connection.on('message', (msg) => {
      // themeRef.current always has the latest value
      logAnalytics('new_message', { roomId, theme: themeRef.current })
      showNotification(msg)
    })
    connection.connect()
    return () => connection.disconnect()
  }, [roomId]) // theme intentionally excluded — but linter warns
}

Essa abordagem funciona, mas o linter sinaliza theme como dependencia ausente. Suprimir o aviso com um comentario oculta bugs potenciais em outras partes do codigo. Alem disso, o padrao com useRef obscurece a intencao: um desenvolvedor novo na equipe precisa rastrear mentalmente por que theme esta armazenado em um ref em vez de constar no array de dependencias.

useEffectEvent: separando logica reativa de nao reativa

O hook useEffectEvent cria uma funcao estavel que sempre le os valores mais recentes de props e estado, sem provocar a re-sincronizacao do efeito. Esse hook substitui o padrao com useRef por uma API declarativa que o linter compreende nativamente.

hooks/useChatRoom.tstsx
// After useEffectEvent: clean separation of concerns
import { useEffect, useEffectEvent } from 'react'

export function useChatRoom(roomId: string, theme: string) {
  // Effect Event: always reads latest theme, never triggers reconnect
  const onMessage = useEffectEvent((msg: string) => {
    logAnalytics('new_message', { roomId, theme })
    showNotification(msg)
  })

  useEffect(() => {
    const connection = createConnection(roomId)
    connection.on('message', onMessage)
    connection.connect()
    return () => connection.disconnect()
  }, [roomId]) // No linter warning — onMessage is an Effect Event
}

O efeito so re-executa quando roomId muda. O callback onMessage sempre acessa o theme atual sem precisar constar como dependencia. A documentacao oficial do React estabelece explicitamente que Effect Events devem ser chamados apenas dentro de efeitos -- nunca durante a renderizacao ou passados como props para componentes filhos.

Regras e restricoes do useEffectEvent

Tres regras governam o uso do useEffectEvent:

  1. A chamada deve ocorrer no nivel superior do componente ou custom hook -- nunca dentro de loops ou condicoes
  2. A funcao retornada so pode ser chamada dentro de useEffect ou de outro Effect Event
  3. A funcao nunca deve ser passada como prop nem retornada de um hook para consumo externo

Violar a regra 2 causa bugs sutis: a identidade da funcao muda a cada renderizacao, entao armazena-la em um ref ou passa-la para componentes filhos anula o proposito do hook. O eslint-plugin-react-hooks@6+ impoe essas restricoes automaticamente.

components/SearchTracker.tsxtsx
// Correct: Effect Event used inside useEffect
import { useEffect, useEffectEvent, useState } from 'react'

interface SearchTrackerProps {
  query: string
  userId: string
}

export function SearchTracker({ query, userId }: SearchTrackerProps) {
  const [results, setResults] = useState<string[]>([])

  // Track searches with current user context
  const onSearchComplete = useEffectEvent((resultCount: number) => {
    analytics.track('search_complete', {
      query,
      userId,       // Always latest userId
      resultCount,
      timestamp: Date.now(),
    })
  })

  useEffect(() => {
    const controller = new AbortController()
    fetchSearchResults(query, controller.signal).then((data) => {
      setResults(data)
      onSearchComplete(data.length) // Called from inside useEffect
    })
    return () => controller.abort()
  }, [query]) // userId excluded safely — lives in the Effect Event

  return <ResultsList results={results} />
}
Quando nao usar useEffectEvent

useEffectEvent nao e uma escapatoria para silenciar o linter de dependencias. Se um valor genuinamente controla quando um efeito deve re-executar, esse valor pertence ao array de dependencias. A extracao de logica para um Effect Event so se justifica quando ela representa uma acao lateral (logging, notificacao, analytics) que le valores reativos sem precisar re-disparar o efeito.

O componente Activity: pre-renderizacao com preservacao de estado

O componente <Activity> (anteriormente chamado de "Offscreen") controla a visibilidade dos seus filhos. Diferentemente da renderizacao condicional, que destroi o estado, ou do CSS display: none, que mantem efeitos em execucao, o <Activity> preserva o estado enquanto limpa efeitos e adia atualizacoes.

components/TabLayout.tsxtsx
// Activity preserves form state across tab switches
import { useState } from 'react'
import { Activity } from 'react'

interface Tab {
  id: string
  label: string
  content: React.ReactNode
}

export function TabLayout({ tabs }: { tabs: Tab[] }) {
  const [activeTab, setActiveTab] = useState(tabs[0].id)

  return (
    <div>
      <nav className="flex gap-2 border-b border-border">
        {tabs.map((tab) => (
          <button
            key={tab.id}
            onClick={() => setActiveTab(tab.id)}
            className={activeTab === tab.id ? 'border-b-2 border-primary' : ''}
          >
            {tab.label}
          </button>
        ))}
      </nav>
      {tabs.map((tab) => (
        <Activity key={tab.id} mode={activeTab === tab.id ? 'visible' : 'hidden'}>
          {tab.content}
        </Activity>
      ))}
    </div>
  )
}

Um usuario preenchendo um formulario na aba "Perfil" pode alternar para "Configuracoes" e voltar sem perder os dados digitados. Os efeitos da aba oculta (timers, subscriptions, data fetching) sao limpos, liberando recursos. Quando a aba se torna visivel novamente, os efeitos sao remontados e o estado e restaurado instantaneamente.

Pronto para mandar bem nas entrevistas de React / Next.js?

Pratique com nossos simuladores interativos, flashcards e testes tecnicos.

Modos do Activity: visivel vs oculto em detalhe

O componente <Activity> aceita a prop mode com dois valores possiveis:

| Comportamento | mode="visible" | mode="hidden" | |----------|-------------------|------------------| | Renderizacao do DOM | Normal | display: none via CSS | | Estado do componente | Ativo | Preservado em memoria | | Efeitos (useEffect) | Montados | Limpos | | Prioridade de atualizacoes | Normal | Adiada para idle | | Pre-renderizacao | N/A | Renderizado em baixa prioridade |

Quando um componente inicia oculto (renderizacao inicial com mode="hidden"), o React o pre-renderiza com prioridade baixa sem montar efeitos. Isso possibilita navegacao instantanea: a pagina de destino ja esta renderizada em segundo plano quando o usuario clica.

components/PrerenderedRoute.tsxtsx
// Pre-render the next likely route in the background
import { Activity, Suspense, use } from 'react'

interface PrerenderedRouteProps {
  isActive: boolean
  dataPromise: Promise<DashboardData>
}

export function PrerenderedRoute({ isActive, dataPromise }: PrerenderedRouteProps) {
  return (
    <Activity mode={isActive ? 'visible' : 'hidden'}>
      <Suspense fallback={<DashboardSkeleton />}>
        <DashboardContent dataPromise={dataPromise} />
      </Suspense>
    </Activity>
  )
}

function DashboardContent({ dataPromise }: { dataPromise: Promise<DashboardData> }) {
  // use() reads the cached promise — works during hidden pre-render
  const data = use(dataPromise)
  return (
    <div className="grid grid-cols-3 gap-4">
      <MetricsCard data={data.metrics} />
      <ChartPanel data={data.charts} />
      <RecentActivity items={data.activity} />
    </div>
  )
}

O <Activity> oculto pre-renderiza o dashboard com prioridade ociosa. Combinado com Suspense e a API use, o carregamento de dados ocorre em segundo plano. Quando isActive muda para true, o conteudo aparece sem nenhum spinner de carregamento.

Activity e TanStack Query: a armadilha do cache

Uma armadilha comum ao usar <Activity> envolve o TanStack Query. Como o useQuery depende internamente de useEffect, queries dentro de um <Activity> oculto nao serao executadas -- o efeito esta desmontado.

components/UserDashboard.tsxtsx
// Problem: useQuery won't fetch when Activity is hidden
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { Activity } from 'react'

function UserStats() {
  // This useQuery will NOT run while hidden
  const { data } = useQuery({
    queryKey: ['user-stats'],
    queryFn: fetchUserStats,
  })
  return <StatsDisplay data={data} />
}

// Solution: prefetch data outside the Activity boundary
function DashboardWithPrefetch({ showStats }: { showStats: boolean }) {
  const queryClient = useQueryClient()

  // Prefetch at the parent level — runs regardless of Activity mode
  queryClient.ensureQueryData({
    queryKey: ['user-stats'],
    queryFn: fetchUserStats,
  })

  return (
    <Activity mode={showStats ? 'visible' : 'hidden'}>
      <UserStats />
    </Activity>
  )
}

A solucao e direta: mover o prefetching de dados para um componente pai ou usar queryClient.ensureQueryData fora da fronteira do <Activity>. Os dados em cache estarao disponiveis quando o componente oculto se tornar visivel.

Trade-off de memoria

O Activity troca memoria por velocidade. Cada arvore de componentes oculta permanece na memoria com seu DOM completo. Para aplicacoes com muitas rotas ocultas, o consumo de memoria deve ser monitorado. A equipe do React esta explorando a evicao automatica de Activities ocultos menos utilizados em versoes futuras.

Combinando useEffectEvent e Activity

Ambas as APIs se complementam em padroes reais de navegacao. Um cenario comum e um dashboard com abas onde cada aba mantem subscricoes WebSocket e rastreamento de analytics.

components/LiveDashboard.tsxtsx
import { useEffect, useEffectEvent, useState } from 'react'
import { Activity } from 'react'

function LiveFeed({ channel, userId }: { channel: string; userId: string }) {
  const [messages, setMessages] = useState<Message[]>([])

  // Analytics tracking with latest userId — no effect re-sync
  const onNewMessage = useEffectEvent((msg: Message) => {
    analytics.track('live_message', { channel, userId })
    setMessages((prev) => [...prev, msg])
  })

  useEffect(() => {
    const ws = new WebSocket(`wss://api.example.com/${channel}`)
    ws.onmessage = (event) => {
      const msg = JSON.parse(event.data) as Message
      onNewMessage(msg)
    }
    return () => ws.close()
  }, [channel]) // Clean reconnect only when channel changes

  return <MessageList messages={messages} />
}

export function LiveDashboard({ userId }: { userId: string }) {
  const [activeChannel, setActiveChannel] = useState('general')
  const channels = ['general', 'alerts', 'metrics']

  return (
    <div>
      <nav className="flex gap-2">
        {channels.map((ch) => (
          <button key={ch} onClick={() => setActiveChannel(ch)}>
            {ch}
          </button>
        ))}
      </nav>
      {channels.map((ch) => (
        <Activity key={ch} mode={activeChannel === ch ? 'visible' : 'hidden'}>
          <LiveFeed channel={ch} userId={userId} />
        </Activity>
      ))}
    </div>
  )
}

Ao trocar de canal, o WebSocket do feed oculto e desconectado (limpeza de efeito via <Activity>). O historico de mensagens sobrevive no estado. O Effect Event onNewMessage garante que o analytics sempre referencia o userId atual sem forcar reconexoes do WebSocket.

Perguntas de entrevista: useEffectEvent e Activity

As perguntas a seguir testam a compreensao das novas APIs e sua interacao com o modelo de renderizacao do React. Elas aparecem com frequencia crescente em entrevistas React de empresas que utilizam React 19.2.

P1: Qual problema o useEffectEvent resolve que o useCallback nao consegue?

O useCallback cria uma funcao memoizada, mas essa funcao ainda precisa ser listada no array de dependencias do efeito. Se alguma de suas proprias dependencias mudar, o callback muda, o que re-dispara o efeito. O useEffectEvent cria uma funcao que sempre le os valores mais recentes sem ser uma dependencia -- o efeito nunca re-executa por causa dela. Essa separacao e impossivel apenas com useCallback.

P2: Um Effect Event pode ser passado como prop para um componente filho?

Nao. Effect Events foram projetados para serem chamados apenas dentro de useEffect ou de outros Effect Events. A identidade da funcao muda a cada renderizacao, entao passa-la como prop causaria re-renderizacoes desnecessarias e quebraria o modelo mental. O plugin de ESLint impoe essa regra.

P3: Como o Activity se diferencia da renderizacao condicional e do CSS display:none?

A renderizacao condicional ({show && <Component />}) desmonta o componente por completo -- o estado e destruido. O CSS display: none oculta visualmente mas mantem todos os efeitos em execucao, consumindo recursos desnecessariamente. O <Activity mode="hidden"> preserva o estado, limpa efeitos, adia atualizacoes para prioridade ociosa e pode pre-renderizar conteudo em segundo plano.

P4: O que acontece com o useEffect dentro de um Activity oculto?

Quando um <Activity> transiciona para mode="hidden", o React executa todas as funcoes de limpeza de efeitos (o retorno do useEffect). Nenhum novo efeito e montado enquanto o componente permanece oculto. Quando se torna visivel novamente, os efeitos sao remontados com o estado preservado. Esse e o motivo pelo qual bibliotecas de data fetching que dependem de useEffect precisam de estrategias de prefetching fora da fronteira do Activity.

P5: Como pre-renderizar uma rota com Activity e Suspense?

O componente da rota deve ser envolvido com <Activity mode="hidden"> e um <Suspense> boundary interno. A API use() ou uma fonte de dados compativel com Suspense e utilizada para o carregamento. O React renderiza a arvore oculta com prioridade baixa, resolvendo o Suspense boundary em segundo plano. Quando o usuario navega e o mode muda para "visible", o conteudo totalmente renderizado aparece instantaneamente, sem estado de carregamento.

P6: O useEffectEvent substitui a regra exhaustive-deps do linter?

Nao. A regra exhaustive-deps continua sendo essencial para capturar dependencias genuinamente ausentes. O useEffectEvent trata um caso especifico: logica que le valores reativos mas nao deve controlar quando o efeito re-executa (analytics, notificacoes, logging). Usa-lo para suprimir todos os avisos de dependencia oculta bugs e anula o proposito da API.

Conclusao

  • O useEffectEvent substitui o padrao com useRef para stale closures em efeitos, com suporte nativo no linter via eslint-plugin-react-hooks@6+
  • Effect Events sempre leem as props e o estado mais recentes sem disparar a re-sincronizacao do efeito -- a utilizacao ideal envolve callbacks de analytics, logging e notificacoes
  • O <Activity> preserva o estado dos componentes enquanto limpa efeitos, oferecendo um meio-termo entre renderizacao condicional e ocultacao via CSS
  • Activities ocultos pre-renderizam com prioridade ociosa, viabilizando navegacao instantanea quando combinados com Suspense e a API use
  • O TanStack Query e outras bibliotecas baseadas em efeitos exigem prefetching fora das fronteiras do Activity, ja que o useEffect nao executa no modo oculto
  • Ambas as APIs estao disponiveis a partir do React 19.2 -- a atualizacao do ESLint e do React deve ser feita em conjunto para suporte completo das ferramentas

Comece a praticar!

Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.

Tags

#react
#useEffectEvent
#activity
#react 19
#hooks
#entrevista

Compartilhar

Artigos relacionados