React 19 useEffectEvent y Activity: nuevas API y preguntas de entrevista 2026

Analisis profundo de las API useEffectEvent y Activity de React 19.2. Solucion de closures obsoletas, pre-renderizado en segundo plano, ejemplos de codigo y preguntas de entrevista tecnica.

Diagrama de la arquitectura de las API useEffectEvent y Activity de React 19.2

Las closures obsoletas (stale closures) representan uno de los errores mas dificiles de diagnosticar en aplicaciones React. Cuando un callback definido dentro de useEffect captura un valor de props o state en el momento de su creacion, ese valor queda congelado aunque el componente se actualice. React 19.2 introduce dos APIs que abordan este problema y mejoran el rendimiento de pre-renderizado: useEffectEvent y el componente <Activity>. Ambas llegaron en la version de octubre 2025 y ya estan transformando la forma en que las aplicaciones React de produccion manejan efectos secundarios y navegacion.

Requisito minimo: React 19.2

useEffectEvent y Activity requieren React 19.2 o superior. Se actualizan con npm install react@latest react-dom@latest. El plugin ESLint eslint-plugin-react-hooks@6+ agrega soporte nativo para useEffectEvent en los arreglos de dependencias.

El problema de las closures obsoletas en los efectos

El modelo de sincronizacion de efectos en React se basa en el arreglo de dependencias. Cuando un valor cambia, el efecto se resincroniza. Sin embargo, existen valores que necesitan leerse sin provocar esa resincronizacion. Este escenario se presenta constantemente en aplicaciones con conexiones en tiempo real, tracking de analytics y notificaciones.

Considerese una aplicacion de chat que registra datos de analytics cuando llega un nuevo mensaje. El registro debe incluir el theme actual, pero un cambio de tema no deberia reconectar el 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
}

El patron con useRef funciona, pero presenta problemas significativos. El linter de ESLint marca una advertencia porque theme se utiliza dentro del efecto pero no aparece en las dependencias. La intencion del desarrollador queda oculta: no resulta evidente por que theme vive en un ref en lugar del arreglo de dependencias. Ademas, este workaround manual agrega codigo repetitivo que oscurece la logica de negocio y confunde a nuevos integrantes del equipo.

useEffectEvent: separacion entre logica reactiva y no reactiva

El hook useEffectEvent crea una funcion estable que siempre lee los valores mas recientes de props y state, sin provocar la resincronizacion del efecto. Reemplaza el patron de useRef con una API declarativa que el linter comprende de forma nativa.

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
}

La diferencia es notable: el codigo expresa claramente su intencion. La funcion onMessage es un Effect Event que gestiona la reaccion ante un mensaje entrante. Lee theme directamente sin ningun workaround. El arreglo de dependencias solo contiene roomId, lo que corresponde exactamente al comportamiento deseado: la conexion unicamente se restablece cuando cambia la sala. La documentacion oficial de React establece de forma explicita que los Effect Events solo deben invocarse desde el interior de efectos, nunca durante el renderizado ni pasados a componentes hijos.

Reglas y restricciones de useEffectEvent

Tres reglas gobiernan el uso de useEffectEvent:

  1. Debe invocarse en el nivel superior de un componente o custom hook, nunca dentro de bucles o condicionales.
  2. La funcion retornada solo puede llamarse desde el interior de useEffect u otro Effect Event.
  3. No debe pasarse como prop ni retornarse desde un hook para consumo externo.

Violar la regla 2 genera errores sutiles: la identidad de la funcion cambia en cada render, por lo que almacenarla en un ref o pasarla a componentes hijos anula su proposito. El plugin eslint-plugin-react-hooks@6+ aplica estas restricciones de manera automatica.

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} />
}
Cuando NO usar useEffectEvent

useEffectEvent no es una via de escape para silenciar el linter de dependencias. Si un valor genuinamente controla cuando un efecto debe re-ejecutarse, ese valor pertenece al arreglo de dependencias. Solo debe extraerse logica en un Effect Event cuando representa una accion secundaria (logging, notificaciones, analytics) que lee valores reactivos sin necesidad de relanzar el efecto.

El componente Activity: pre-renderizado en segundo plano con preservacion de estado

El componente <Activity> (anteriormente llamado "Offscreen") controla si sus hijos son visibles u ocultos. A diferencia del renderizado condicional que destruye el estado, o de CSS display: none que mantiene los efectos activos, <Activity> preserva el estado mientras limpia los efectos y difiere las actualizaciones.

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>
  )
}

A diferencia de los enfoques tradicionales con renderizado condicional ({activeTab === tab.id && <Content />}), el componente Activity preserva el estado interno de las pestanas ocultas. Un formulario parcialmente completado, una posicion de scroll o un estado de componente local sobreviven al cambio entre pestanas. Los efectos de la pestana oculta (timers, suscripciones, data fetching) se limpian, liberando recursos. Cuando la pestana vuelve a ser visible, los efectos se remontan y el estado se restaura de manera instantanea.

¿Listo para aprobar tus entrevistas de React / Next.js?

Practica con nuestros simuladores interactivos, flashcards y tests técnicos.

Modos de Activity: visible vs hidden en detalle

El componente <Activity> acepta una prop mode con dos valores:

| Comportamiento | mode="visible" | mode="hidden" | |----------|-------------------|------------------| | Renderizado del DOM | Normal | display: none via CSS | | Estado del componente | Activo | Preservado en memoria | | Efectos (useEffect) | Montados | Limpiados | | Prioridad de actualizaciones | Normal | Diferida a tiempo idle | | Pre-renderizado | N/A | Renderizado a baja prioridad |

Cuando un componente inicia oculto (render inicial con mode="hidden"), React lo pre-renderiza a baja prioridad sin montar efectos. Esto habilita la navegacion instantanea: la pagina destino ya esta renderizada en segundo plano cuando el usuario hace clic.

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>
  )
}

El <Activity> oculto pre-renderiza el dashboard a prioridad idle. Combinado con Suspense y la API use, el data fetching ocurre en segundo plano. Cuando isActive cambia a true, el contenido aparece sin ningun spinner de carga.

Activity y TanStack Query: la trampa del cache

Un error frecuente con <Activity> involucra a TanStack Query. Dado que useQuery depende internamente de useEffect, las queries dentro de un <Activity> oculto no se ejecutan porque el efecto 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>
  )
}

La solucion consiste en mover el prefetch de datos al componente padre, fuera de la frontera de Activity. El metodo ensureQueryData del QueryClient dispara el fetch si los datos no estan en cache, garantizando que estaran disponibles de forma instantanea cuando el componente se haga visible.

Compromiso de memoria

Activity intercambia memoria por velocidad. Cada arbol de componentes oculto permanece en memoria con su DOM completo. En aplicaciones con muchas rutas ocultas, es necesario monitorear el consumo de memoria. El equipo de React esta explorando la eviccion automatica de Activities ocultos menos recientemente utilizados en futuras versiones.

Combinando useEffectEvent y Activity

Ambas APIs se complementan en patrones de navegacion del mundo real. Un escenario comun: un dashboard con pestanas donde cada pestana mantiene suscripciones WebSocket y tracking 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>
  )
}

En este ejemplo, useEffectEvent garantiza que el tracking de analytics siempre utilice el userId actual sin reconectar el WebSocket. El componente Activity preserva el estado de mensajes de cada canal, permitiendo una navegacion fluida sin perdida de datos.

Al cambiar de canal, el WebSocket del canal oculto se cierra (limpieza del efecto via <Activity>). El historial de mensajes sobrevive en el state. La funcion onNewMessage como Effect Event asegura que el analytics siempre referencie el userId vigente sin forzar reconexiones del WebSocket.

Preguntas de entrevista: useEffectEvent y Activity

Estas preguntas evaluan la comprension de las nuevas APIs y su interaccion con el modelo de renderizado de React. Son cada vez mas frecuentes en entrevistas tecnicas de React en empresas que utilizan React 19.2.

P1: Que problema resuelve useEffectEvent que useCallback no puede resolver?

useCallback crea una funcion memoizada, pero esa funcion aun necesita incluirse en el arreglo de dependencias del efecto. Si alguna de sus propias dependencias cambia, el callback cambia, lo que re-dispara el efecto. useEffectEvent crea una funcion que siempre lee los valores mas recientes sin ser una dependencia: el efecto nunca se re-ejecuta por causa de esa funcion. Esta separacion es imposible de lograr unicamente con useCallback.

P2: Puede pasarse un Effect Event como prop a un componente hijo?

No. Los Effect Events estan disenados para invocarse exclusivamente desde el interior de useEffect u otros Effect Events. Su identidad cambia en cada render, por lo que pasarlos como props causaria re-renders innecesarios y romperia el modelo mental. El plugin de ESLint aplica esta regla.

P3: En que se diferencia Activity del renderizado condicional y de CSS display:none?

El renderizado condicional ({show && <Component />}) desmonta el componente por completo y destruye su estado. CSS display: none oculta visualmente pero mantiene todos los efectos activos, desperdiciando recursos. <Activity mode="hidden"> preserva el estado, limpia los efectos, difiere las actualizaciones a prioridad idle y permite pre-renderizar contenido en segundo plano.

P4: Que sucede con useEffect dentro de un Activity oculto?

Cuando un <Activity> transiciona a mode="hidden", React ejecuta todas las funciones de limpieza de los efectos (el valor de retorno de useEffect). Ningun efecto nuevo se monta mientras el componente permanece oculto. Cuando el componente vuelve a ser visible, los efectos se remontan con el estado preservado. Por esta razon, las bibliotecas de data fetching que dependen de useEffect necesitan estrategias de prefetch fuera de la frontera de Activity.

P5: Como se pre-renderiza una ruta con Activity y Suspense?

Se envuelve la ruta en <Activity mode="hidden"> con un <Suspense> en su interior. Utilizando la API use() o una fuente de datos compatible con Suspense para el fetching, React renderiza el arbol oculto a baja prioridad, resolviendo la frontera de Suspense en segundo plano. Cuando el usuario navega y el mode cambia a "visible", el contenido completamente renderizado aparece de forma instantanea sin estado de carga.

P6: useEffectEvent reemplaza la regla exhaustive-deps del linter?

No. La regla exhaustive-deps sigue siendo fundamental para detectar dependencias faltantes genuinas. useEffectEvent maneja un caso especifico: logica que lee valores reactivos pero no debe controlar cuando el efecto se re-ejecuta (analytics, notificaciones, logging). Utilizarlo para suprimir todas las advertencias de dependencias oculta bugs y contradice su proposito.

Conclusion

  • useEffectEvent reemplaza el workaround de useRef para closures obsoletas en efectos, con soporte nativo del linter en eslint-plugin-react-hooks@6+
  • Los Effect Events siempre leen los valores mas recientes de props y state sin provocar resincronizacion del efecto: deben usarse para analytics, logging y callbacks de notificacion
  • <Activity> preserva el estado de los componentes mientras limpia los efectos, ofreciendo un punto intermedio entre el renderizado condicional y el ocultamiento via CSS
  • Los Activity ocultos pre-renderizan a prioridad baja, habilitando navegacion instantanea al combinarse con Suspense y la API use
  • TanStack Query y otras bibliotecas basadas en efectos requieren prefetch fuera de las fronteras de Activity, dado que useEffect no se ejecuta en modo oculto
  • Ambas APIs se incluyen en React 19.2: es necesario actualizar ESLint y React en conjunto para obtener soporte completo de herramientas

¡Empieza a practicar!

Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.

Etiquetas

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

Compartir

Artículos relacionados