React 19 useEffectEvent e Activity: Nuove API e Domande da Colloquio 2026

Guida approfondita a useEffectEvent e al componente Activity di React 19.2 – con esempi di codice, best practice e domande frequenti nei colloqui tecnici 2026.

Panoramica delle API useEffectEvent e Activity di React 19

React 19.2 ha introdotto due API che affrontano problemi di vecchia data: useEffectEvent elimina le stale closure negli effect, mentre <Activity> consente il pre-rendering in background con preservazione completa dello state. Entrambe le API sono state rilasciate a ottobre 2025 e stanno già modificando il modo in cui le applicazioni React in produzione gestiscono side effect e navigazione.

Requisito minimo: React 19.2

useEffectEvent e Activity richiedono React 19.2 o versione successiva. L'aggiornamento si esegue con npm install react@latest react-dom@latest. Il plugin ESLint eslint-plugin-react-hooks@6+ aggiunge supporto nativo per useEffectEvent nei dependency array.

Il problema delle stale closure: perch\u00e9 serve useEffectEvent

Ogni sviluppatore React ha incontrato almeno una volta il problema delle stale closure. Un effect cattura un valore al momento del rendering e, quando quel valore cambia, l'effect continua a fare riferimento a quello vecchio. La soluzione classica prevedeva l'utilizzo di useRef per mantenere un riferimento mutabile: funzionale, ma verboso e invisibile al linter.

Si consideri un'applicazione di chat che registra analytics all'arrivo di un nuovo messaggio. Il log deve includere il theme corrente, ma i cambiamenti del tema non devono riconnettere la chat:

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

export function useChatRoom(roomId: string, theme: string) {
  // Salvare il theme in un ref per evitare la stale closure
  const themeRef = useRef(theme)
  themeRef.current = theme

  useEffect(() => {
    const connection = createConnection(roomId)
    connection.on('message', (msg) => {
      // themeRef.current ha sempre il valore aggiornato
      logAnalytics('new_message', { roomId, theme: themeRef.current })
      showNotification(msg)
    })
    connection.connect()
    return () => connection.disconnect()
  }, [roomId]) // theme escluso intenzionalmente — ma il linter avvisa
}

Questo approccio funziona, ma il linter segnala theme come dipendenza mancante. Sopprimere l'avviso nasconde potenziali bug altrove. Il pattern useRef oscura anche l'intento: chi legge il codice per la prima volta deve ricostruire mentalmente il motivo per cui theme si trova in un ref invece che nel dependency array.

useEffectEvent: separare la logica reattiva da quella non reattiva

L'hook useEffectEvent crea una funzione stabile che legge sempre le props e lo state pi\u00f9 recenti, senza attivare la risincronizzazione dell'effect. Sostituisce il pattern useRef con un'API dichiarativa che il linter comprende nativamente.

hooks/useChatRoom.tstsx
// Dopo useEffectEvent: separazione netta delle responsabilit\u00e0
import { useEffect, useEffectEvent } from 'react'

export function useChatRoom(roomId: string, theme: string) {
  // Effect Event: legge sempre il theme corrente, non provoca 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]) // Nessun avviso del linter — onMessage \u00e8 un Effect Event
}

L'effect viene rieseguito solo quando roomId cambia. Il callback onMessage vede sempre il theme corrente senza essere elencato come dipendenza. La documentazione ufficiale di React stabilisce esplicitamente che gli Effect Event devono essere chiamati solo dall'interno degli effect, mai durante il rendering o passati ai componenti figli.

Regole e vincoli di useEffectEvent

Tre regole governano useEffectEvent:

  1. Va chiamato al livello superiore di un componente o di un custom hook, mai all'interno di loop o condizioni
  2. La funzione restituita pu\u00f2 essere chiamata solo all'interno di useEffect o di un altro Effect Event
  3. Non deve essere passata come prop n\u00e9 restituita da un hook per uso esterno

Violare la regola 2 causa bug sottili: l'identit\u00e0 della funzione cambia a ogni render, quindi salvarla in un ref o passarla verso il basso vanifica lo scopo. Il plugin eslint-plugin-react-hooks@6+ applica questi vincoli automaticamente.

components/SearchTracker.tsxtsx
// Corretto: Effect Event utilizzato all'interno di useEffect
import { useEffect, useEffectEvent, useState } from 'react'

interface SearchTrackerProps {
  query: string
  userId: string
}

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

  // Tracciare le ricerche con il contesto utente corrente
  const onSearchComplete = useEffectEvent((resultCount: number) => {
    analytics.track('search_complete', {
      query,
      userId,       // Sempre la userId aggiornata
      resultCount,
      timestamp: Date.now(),
    })
  })

  useEffect(() => {
    const controller = new AbortController()
    fetchSearchResults(query, controller.signal).then((data) => {
      setResults(data)
      onSearchComplete(data.length) // Chiamato dall'interno di useEffect
    })
    return () => controller.abort()
  }, [query]) // userId esclusa in sicurezza — risiede nell'Effect Event

  return <ResultsList results={results} />
}
Non \u00e8 un sostituto del linter

useEffectEvent non \u00e8 una scappatoia per silenziare il linter delle dipendenze. Se un valore controlla realmente quando un effect deve rieseguirsi, appartiene al dependency array. La logica va estratta in un Effect Event solo quando rappresenta un'azione secondaria (logging, notifiche, analytics) che legge valori reattivi senza dover riattivare l'effect.

Il componente Activity: pre-rendering in background con preservazione dello state

Il componente <Activity> (precedentemente denominato "Offscreen") controlla se i suoi figli sono visibili o nascosti. A differenza del rendering condizionale che distrugge lo state, o di CSS display: none che mantiene gli effect in esecuzione, <Activity> preserva lo state mentre esegue il cleanup degli effect e posticipa gli aggiornamenti.

components/TabLayout.tsxtsx
// Activity preserva lo state del form durante il cambio tab
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>
  )
}

Se un utente sta compilando un form nella scheda "Profilo", pu\u00f2 passare alla scheda "Impostazioni" e tornare indietro senza perdere i dati inseriti. Gli effect della scheda nascosta (timer, subscription, fetch di dati) vengono ripuliti, liberando risorse. Quando la scheda torna visibile, gli effect vengono rimontati e lo state viene ripristinato istantaneamente.

Pronto a superare i tuoi colloqui su React / Next.js?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Modalit\u00e0 Activity: visible e hidden nel dettaglio

Il componente <Activity> accetta una prop mode con due valori:

| Comportamento | mode="visible" | mode="hidden" | |----------|-------------------|------------------| | Rendering DOM | Normale | display: none via CSS | | State del componente | Attivo | Preservato in memoria | | Effect (useEffect) | Montati | Ripuliti | | Priorit\u00e0 aggiornamenti | Normale | Posticipata a idle | | Pre-rendering | N/A | Rendering a bassa priorit\u00e0 |

Quando un componente parte nascosto (primo render con mode="hidden"), React lo renderizza a bassa priorit\u00e0 senza montare gli effect. Questo abilita la navigazione istantanea: la pagina di destinazione \u00e8 gi\u00e0 renderizzata in background quando l'utente fa clic.

components/PrerenderedRoute.tsxtsx
// Pre-renderizzare la prossima route probabile in 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() legge la promise in cache — funziona durante il pre-render nascosto
  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>
  )
}

L'<Activity> nascosta pre-renderizza la dashboard a priorit\u00e0 idle. Combinata con Suspense e l'API use, il caricamento dei dati avviene in background. Quando isActive passa a true, il contenuto appare senza alcun indicatore di caricamento.

Activity e TanStack Query: l'insidia della cache

Un errore frequente con <Activity> riguarda TanStack Query. Poich\u00e9 useQuery si basa internamente su useEffect, le query all'interno di un'<Activity> nascosta non verranno eseguite: l'effect \u00e8 smontato.

components/UserDashboard.tsxtsx
// Problema: useQuery non effettua il fetch quando Activity \u00e8 nascosta
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { Activity } from 'react'

function UserStats() {
  // Questa useQuery NON verr\u00e0 eseguita mentre \u00e8 nascosta
  const { data } = useQuery({
    queryKey: ['user-stats'],
    queryFn: fetchUserStats,
  })
  return <StatsDisplay data={data} />
}

// Soluzione: precaricare i dati al di fuori del confine Activity
function DashboardWithPrefetch({ showStats }: { showStats: boolean }) {
  const queryClient = useQueryClient()

  // Prefetch al livello genitore — eseguito indipendentemente dalla modalit\u00e0 Activity
  queryClient.ensureQueryData({
    queryKey: ['user-stats'],
    queryFn: fetchUserStats,
  })

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

La soluzione \u00e8 diretta: spostare il prefetch dei dati in un componente genitore o utilizzare queryClient.ensureQueryData al di fuori del confine <Activity>. I dati in cache saranno disponibili quando il componente nascosto torner\u00e0 visibile.

Compromesso sulla memoria

Activity scambia memoria per velocit\u00e0. Ogni albero di componenti nascosto rimane in memoria con il suo DOM completo. Per applicazioni con molte route nascoste, \u00e8 consigliabile monitorare il consumo di memoria. Il team React sta esplorando l'eliminazione automatica delle Activity nascoste meno utilizzate nelle versioni future.

Combinare useEffectEvent e Activity

Entrambe le API si completano a vicenda nei pattern di navigazione reali. Uno scenario comune: una dashboard con tab in cui ogni scheda ha subscription WebSocket e tracking 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[]>([])

  // Tracking analytics con userId corrente — nessuna risincronizzazione dell'effect
  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]) // Riconnessione pulita solo al cambio canale

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

Durante il cambio canale, il WebSocket del feed nascosto viene disconnesso (cleanup dell'effect tramite <Activity>). La cronologia dei messaggi sopravvive nello state. L'Effect Event onNewMessage garantisce che gli analytics facciano sempre riferimento alla userId corrente senza forzare riconnessioni WebSocket.

Domande da colloquio: useEffectEvent e Activity

Queste domande testano la comprensione delle nuove API e della loro interazione con il modello di rendering di React. Sono sempre pi\u00f9 comuni nei colloqui React presso aziende che utilizzano React 19.2.

D1: Quale problema risolve useEffectEvent che useCallback non pu\u00f2 risolvere?

useCallback crea una funzione memoizzata, che deve comunque essere inserita nel dependency array dell'effect. Se una delle sue dipendenze cambia, il callback cambia, il che riesegue l'effect. useEffectEvent crea una funzione che legge sempre i valori pi\u00f9 recenti senza essere una dipendenza: l'effect non viene mai rieseguito per causa sua. Questa separazione \u00e8 impossibile con il solo useCallback.

D2: Un Effect Event pu\u00f2 essere passato come prop a un componente figlio?

No. Gli Effect Event sono progettati per essere chiamati esclusivamente dall'interno di useEffect o di altri Effect Event. La loro identit\u00e0 cambia a ogni render, quindi passarli come props causerebbe re-render non necessari e infrangerebbe il modello mentale. Il plugin ESLint applica questa regola.

D3: In che modo Activity differisce dal rendering condizionale e da CSS display:none?

Il rendering condizionale ({show && <Component />}) smonta completamente il componente, distruggendo lo state. CSS display: none nasconde visivamente ma mantiene tutti gli effect in esecuzione, sprecando risorse. <Activity mode="hidden"> preserva lo state, esegue il cleanup degli effect, posticipa gli aggiornamenti a priorit\u00e0 idle e pu\u00f2 pre-renderizzare il contenuto in background.

D4: Cosa succede a useEffect all'interno di un Activity nascosto?

Quando un <Activity> passa a mode="hidden", React esegue tutte le funzioni di cleanup degli effect (il valore di ritorno di useEffect). Nessun nuovo effect viene montato finch\u00e9 il componente resta nascosto. Quando torna visibile, gli effect si rimontano con lo state preservato. Per questo le librerie di data fetching che si basano su useEffect necessitano di strategie di prefetching al di fuori del confine Activity.

D5: Come si pre-renderizza una route con Activity e Suspense?

La route va avvolta in <Activity mode="hidden"> con un boundary <Suspense> all'interno. Per il fetch dei dati si utilizza l'API use() o una sorgente dati compatibile con Suspense. React renderizza l'albero nascosto a bassa priorit\u00e0, risolvendo il boundary Suspense in background. Quando l'utente naviga e mode diventa "visible", il contenuto completamente renderizzato appare istantaneamente senza stato di caricamento.

D6: useEffectEvent \u00e8 un sostituto della regola lint exhaustive-deps?

No. La regola exhaustive-deps rimane fondamentale per individuare dipendenze genuinamente mancanti. useEffectEvent gestisce un caso specifico: logica che legge valori reattivi ma non deve controllare quando l'effect si riesegue (analytics, notifiche, logging). Utilizzarlo per sopprimere tutti gli avvisi sulle dipendenze nasconde bug e ne vanifica lo scopo.

Conclusione

  • useEffectEvent sostituisce il workaround con useRef per le stale closure negli effect, con supporto nativo del linter in eslint-plugin-react-hooks@6+
  • Gli Effect Event leggono sempre le props e lo state pi\u00f9 recenti senza attivare la risincronizzazione dell'effect \u2014 ideali per analytics, logging e callback di notifica
  • <Activity> preserva lo state dei componenti eseguendo il cleanup degli effect, offrendo un compromesso tra rendering condizionale e nascondimento CSS
  • Le Activity nascoste pre-renderizzano a priorit\u00e0 idle, abilitando la navigazione istantanea in combinazione con Suspense e l'API use
  • TanStack Query e altre librerie basate su effect necessitano di prefetching al di fuori dei confini Activity, poich\u00e9 useEffect non viene eseguito in modalit\u00e0 nascosta
  • Entrambe le API sono disponibili a partire da React 19.2 \u2014 aggiornare ESLint e React insieme per il pieno supporto degli strumenti

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Tag

#react
#react-19
#useEffectEvent
#activity
#colloquio

Condividi

Articoli correlati