React 19 useEffectEvent ve Activity: Yeni API'ler ve 2026 Mulakat Sorulari

React 19.2 useEffectEvent ve Activity API detayli incelemesi. Stale closure cozumleri, arka plan on-render, kod ornekleri ve 2026 mulakat sorulari.

React 19.2 useEffectEvent ve Activity API mimarisi diyagrami

React 19.2, uzun süredir deneysel aşamada olan iki önemli API'yi kararlı sürüme taşıdı: useEffectEvent ve Activity. Bu iki API, React uygulamalarında sıkça karşılaşılan iki temel soruna çözüm sunuyor. useEffectEvent, effect içindeki stale closure problemini ortadan kaldırırken, Activity bileşeni arka planda ön-render işlemleri gerçekleştirerek kullanıcı deneyimini iyileştiriyor. Bu makale, her iki API'nin çalışma prensiplerini, kullanım kurallarını, yaygın tuzaklarını ve teknik mülakatlarda karşılaşılabilecek soruları kapsamlı şekilde ele almaktadır.

React 19.2 Gereksinimi

useEffectEvent ve Activity API'leri yalnızca React 19.2 ve sonraki sürümlerde kararlı olarak kullanılabilir. Proje bağımlılıklarının en az react@19.2.0 sürümüne güncellendiğinden emin olunmalıdır.

Stale Closure Problemi ve useEffectEvent Çözümü

React geliştiricilerinin en sık karşılaştığı sorunlardan biri, useEffect içindeki callback fonksiyonlarının eski (stale) değerlere referans vermesidir. Bu durum, bir effect'in bağımlılık dizisinde yer almayan ancak callback içinde kullanılan değişkenlerin güncel halini okuyamamasından kaynaklanır. Geleneksel çözüm, useRef kullanarak değeri manuel olarak güncel tutmaktı; ancak bu yaklaşım hem okunabilirliği azaltır hem de linter uyarılarına neden olur.

Aşağıdaki örnekte, bir sohbet odası bağlantısı yönetilirken theme değerinin güncel kalması için useRef ile geçici bir çözüm uygulanmıştır:

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
}

Bu kod çalışıyor olsa da, birkaç ciddi dezavantajı bulunmaktadır. İlk olarak, theme bağımlılık dizisinden kasıtlı olarak çıkarıldığı için ESLint exhaustive-deps kuralı uyarı verir. İkinci olarak, useRef ile yapılan bu geçici çözüm, kodun asıl amacını gizler ve bakım maliyetini artırır. Üçüncü olarak, ekip üyelerinin bu pattern'i anlaması ve doğru uygulaması gerekir, bu da hata riskini yükseltir.

useEffectEvent hook'u bu sorunu kökünden çözer. Bir Effect Event olarak tanımlanan fonksiyon, her zaman en güncel prop ve state değerlerini okur, ancak effect'in yeniden çalışmasını tetiklemez:

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
}

Bu yaklaşımda theme değişkeni doğrudan useEffectEvent callback'i içinde okunmaktadır. React, bu fonksiyonun bir Effect Event olduğunu bildiği için, theme değişimlerinde effect'i yeniden çalıştırmaz ancak callback çağrıldığında her zaman güncel theme değerini kullanır. Linter uyarısı da ortadan kalkar, çünkü onMessage bir Effect Event olarak tanınır ve bağımlılık dizisine eklenmesi gerekmez.

useEffectEvent Kuralları ve Kısıtlamaları

useEffectEvent son derece güçlü bir araç olsa da, belirli kurallar dahilinde kullanılmalıdır. Bu kurallar, API'nin doğru çalışmasını garanti altına alır ve olası hataların önüne geçer.

Kural 1: Yalnızca useEffect içinden çağrılmalıdır. Bir Effect Event, doğrudan event handler olarak veya render sırasında çağrılamaz. Yalnızca bir useEffect bloğunun içinden tetiklenmelidir.

Kural 2: Asla başka bileşenlere veya hook'lara aktarılmamalıdır. Effect Event fonksiyonları, tanımlandıkları bileşenin kapsamında kalmalıdır. Props olarak alt bileşenlere geçirilmesi veya başka hook'lara parametre olarak verilmesi desteklenmez.

Kural 3: Bağımlılık dizisine dahil edilmez. React, useEffectEvent ile oluşturulan fonksiyonları özel olarak işler ve bunların bağımlılık dizisine eklenmesini gerektirmez.

Aşağıdaki örnek, bu kuralların doğru uygulandığı bir arama izleme bileşenini göstermektedir:

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

Bu örnekte userId değişkeni bağımlılık dizisinden güvenle çıkarılmıştır. Kullanıcı kimliği değiştiğinde arama yeniden tetiklenmez, ancak analitik izleme her zaman güncel userId değerini kullanır. Bu, reaktif davranış (arama sorgusuna tepki verme) ile reaktif olmayan davranış (güncel bağlamı okuma) arasındaki ayrımı net bir şekilde ortaya koyar.

Yaygın Hata: Render Sırasında Çağırma

useEffectEvent ile oluşturulan fonksiyonların render sırasında veya JSX içinden doğrudan çağrılması çalışma zamanı hatasına neden olur. Bu fonksiyonlar yalnızca useEffect callback'i içinden çağrılmalıdır. onClick gibi DOM event handler'larında kullanılması gereken durumlarda, standart bir fonksiyon tanımı tercih edilmelidir.

Activity Bileşeni: Arka Planda Ön-Render

Activity, React 19.2 ile birlikte gelen ve bileşenlerin arka planda ön-render edilmesini sağlayan yeni bir yerleşik bileşendir. Daha önce <Offscreen> adıyla deneysel olarak geliştirilen bu API, artık kararlı sürümde Activity adıyla kullanılmaktadır. Temel amacı, kullanıcının henüz görmediği ancak kısa süre içinde görme ihtimali yüksek olan içerikleri önceden hazırlamaktır.

Activity bileşeni, mode prop'u aracılığıyla iki farklı modda çalışır: 'visible' ve 'hidden'. visible modunda bileşen normal şekilde render edilir ve kullanıcıya gösterilir. hidden modunda ise bileşen DOM'a düşük öncelikle render edilir, ancak kullanıcıya gösterilmez. Önemli olan nokta, hidden modundaki bileşenlerin state'inin korunmasıdır. Bu sayede bir bileşen hidden'dan visible'a geçtiğinde, tüm form verileri, scroll pozisyonu ve yerel state aynen kaldığı yerden devam eder.

Aşağıdaki örnek, sekme geçişlerinde form durumunun korunmasını sağlayan bir tab layout bileşenini göstermektedir:

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

Bu yaklaşımda her sekme her zaman render edilmektedir, ancak yalnızca aktif sekme kullanıcıya gösterilir. Geleneksel koşullu render yaklaşımında ({activeTab === tab.id && tab.content}) sekme değiştirildiğinde bileşen unmount olur ve tüm yerel state kaybolur. Activity ile bu sorun tamamen ortadan kalkar.

Ön-Render ile Rota Hazırlama

Activity bileşeninin en güçlü kullanım alanlarından biri, kullanıcının bir sonraki adımda ziyaret etme olasılığı yüksek olan rotaları arka planda hazırlamaktır. Bu teknik, algılanan yükleme süresini önemli ölçüde azaltır:

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

Bu örnekte use() hook'u, promise'i okuyarak veriyi çözer. hidden modunda bu işlem arka planda gerçekleşir ve kullanıcı ilgili rotaya geçtiğinde içerik anında görüntülenir. Suspense ile birlikte kullanıldığında, veri henüz hazır değilken iskelet (skeleton) bileşeni gösterilir ve veri geldiğinde otomatik olarak asıl içerik ile değiştirilir.

React / Next.js mülakatlarında başarılı olmaya hazır mısın?

İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.

Activity ve TanStack Query Tuzağı

Activity bileşeni ile TanStack Query birlikte kullanıldığında dikkat edilmesi gereken kritik bir davranış bulunmaktadır. hidden modundaki bir Activity içindeki useQuery hook'ları veri çekme işlemini gerçekleştirmez. Bu, React'in hidden bileşenlerdeki effect'leri geciktirmesinden kaynaklanır ve beklenmedik boş state durumlarına yol açabilir.

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

Çözüm, veri çekme işlemini Activity sınırının dışına taşımaktır. queryClient.ensureQueryData() yöntemi, üst bileşen seviyesinde çağrılarak Activity modundan bağımsız şekilde çalışır. Böylece bileşen visible moduna geçtiğinde, TanStack Query önbelleğinde zaten mevcut olan veriyi anında okur. Bu pattern, TanStack Query ile ilgili detaylı bilgiler için referans kaynağında incelenebilir.

Genel Kural: Effect'ler ve hidden Mod

Activity bileşeni hidden modundayken yalnızca render işlemi gerçekleşir; useEffect, useLayoutEffect ve bunlara bağlı abonelikler (TanStack Query dahil) çalışmaz. Bu davranış, arka plan render'ının yan etki üretmemesi ilkesine dayanır. Veri çekme işlemleri her zaman Activity sınırının dışında başlatılmalıdır.

useEffectEvent ve Activity Birlikte Kullanımı

Bu iki API'nin birlikte kullanıldığı senaryolar, özellikle gerçek zamanlı uygulamalarda güçlü bir mimari sunar. Aşağıdaki örnekte, çok kanallı bir canlı yayın paneli hem WebSocket bağlantı yönetimini hem de sekme geçişlerini sorunsuz şekilde gerçekleştirmektedir:

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

Bu mimaride useEffectEvent, userId değiştiğinde WebSocket bağlantısının yeniden kurulmasını engellerken, Activity bileşeni kanal geçişlerinde mesaj geçmişinin kaybolmasını önler. Kullanıcı bir kanaldan diğerine geçip geri döndüğünde, tüm mesajlar yerinde kalır. hidden moddaki kanalların effect'leri çalışmadığından, gereksiz WebSocket bağlantıları da oluşmaz. Bu durum, performans ve kaynak yönetimi açısından önemli bir avantaj sağlar.

Teknik Mülakat Soruları

Bu bölümde, React Hooks mülakat soruları kapsamında useEffectEvent ve Activity API'leri ile ilgili sıkça sorulan soruları ve beklenen yanıtları ele alınmaktadır.

Soru 1: useEffectEvent, useCallback'ten ne açıdan farklıdır?

useCallback, memoize edilmiş bir fonksiyon döndürür ve bağımlılık dizisi değiştiğinde fonksiyonu yeniden oluşturur. Effect'in bağımlılık dizisine dahil edilmesi gerekir ve dahil edildiğinde effect'in yeniden çalışmasını tetikleyebilir. useEffectEvent ise her zaman güncel prop ve state değerlerini okuyan, ancak effect'in bağımlılık dizisine dahil edilmeyen ve effect'in yeniden çalışmasını asla tetiklemeyen bir fonksiyon döndürür.

Soru 2: Bir useEffectEvent fonksiyonu doğrudan bir onClick handler olarak kullanılabilir mi?

Hayır, kullanılamaz. useEffectEvent ile oluşturulan fonksiyonlar yalnızca useEffect içinden çağrılabilir. DOM event handler'larında doğrudan kullanım desteklenmez ve çalışma zamanı hatasına neden olur. Bu kuralın amacı, reaktif ve reaktif olmayan kod arasındaki sınırı netleştirmektir.

Soru 3: Activity bileşeni hidden modundayken useEffect çalışır mı?

Hayır, çalışmaz. hidden modunda React yalnızca bileşeni render eder, ancak effect'leri, layout effect'lerini ve bunlara bağlı abonelikleri çalıştırmaz. Effect'ler yalnızca bileşen visible moduna geçtiğinde tetiklenir. Bu davranış, arka plan render'ının yan etki üretmemesini garanti eder.

Soru 4: Activity ile koşullu render arasındaki temel fark nedir?

Koşullu render ({condition && <Component />}) bileşeni tamamen unmount eder ve yeniden mount ettiğinde tüm yerel state sıfırlanır. Activity ise bileşeni DOM'da tutarak state'ini korur. Form verileri, scroll pozisyonu ve bileşen içi değişkenler, mod geçişlerinde aynen kalır.

Soru 5: TanStack Query, hidden bir Activity içinde neden veri çekmez ve bu nasıl çözülür?

TanStack Query, useQuery hook'unun effect'lerine bağlıdır. hidden modunda effect'ler çalışmadığından, veri çekme işlemi başlatılmaz. Çözüm olarak queryClient.ensureQueryData() veya queryClient.prefetchQuery() yöntemleri Activity sınırının dışında çağrılmalıdır. Böylece veri, bileşenin mod durumundan bağımsız olarak hazır hale gelir.

Soru 6: useEffectEvent ve Activity birlikte kullanıldığında WebSocket bağlantı yönetimi nasıl etkilenir?

Activity bileşeni hidden modundayken effect'ler çalışmaz, dolayısıyla yeni WebSocket bağlantıları oluşturulmaz. Bileşen visible moduna geçtiğinde effect çalışır ve bağlantı kurulur. useEffectEvent sayesinde bağlantı callback'i her zaman güncel değerleri okur, ancak bağlantının kendisi yalnızca asıl bağımlılıklar (örneğin kanal adı) değiştiğinde yeniden kurulur. Bu kombinasyon, gereksiz bağlantı ve yeniden bağlantı maliyetini ortadan kaldırır.

Sonuc

useEffectEvent ve Activity, React 19.2 ile birlikte gelen ve farklı ancak birbirini tamamlayan sorunları çözen iki temel API'dir. useEffectEvent, stale closure sorununu temiz bir şekilde ortadan kaldırarak effect'lerdeki reaktif ve reaktif olmayan mantığın ayrılmasını sağlar. Activity ise bileşen state'ini koruyarak ve arka planda ön-render yaparak kullanıcı deneyimini önemli ölçüde iyileştirir. Her iki API'nin kurallarını ve kısıtlamalarını anlamak, hem üretim kodunda hatasız kullanım hem de teknik mülakatlarda başarılı performans için kritik öneme sahiptir. Detaylı bilgi için React 19.2 resmi duyurusu ve useEffectEvent referans dokümantasyonu incelenebilir.

Pratik yapmaya başla!

Mülakat simülatörleri ve teknik testlerle bilgini test et.

Etiketler

#react
#useEffectEvent
#activity
#react 19
#hooks
#interview

Paylaş

İlgili makaleler