React 19 useEffectEvent dan Activity: API Baru dan Pertanyaan Interview 2026
Pembahasan mendalam useEffectEvent dan komponen Activity di React 19.2. Solusi stale closure, pre-rendering di background, contoh kode, dan pertanyaan interview teknis.

React 19.2 memperkenalkan dua API yang mengatasi permasalahan lama dalam ekosistem React: useEffectEvent menghilangkan stale closure pada effect, dan <Activity> memungkinkan pre-rendering di background dengan preservasi state. Kedua fitur ini dirilis pada Oktober 2025 dan telah mengubah cara aplikasi React production menangani side effect dan navigasi.
useEffectEvent dan Activity memerlukan React 19.2 atau versi lebih baru. Update dengan npm install react@latest react-dom@latest. Plugin ESLint eslint-plugin-react-hooks@6+ menambahkan dukungan native untuk useEffectEvent dalam dependency array.
Masalah yang Diselesaikan useEffectEvent: Stale Closure
Setiap developer React pernah menghadapi stale closure. Sebuah effect menangkap nilai pada saat render, dan ketika nilai tersebut berubah, effect masih mereferensikan nilai yang lama. Solusi klasik menggunakan useRef untuk menyimpan referensi mutable, pendekatan yang fungsional namun verbose dan tidak terdeteksi oleh linter.
Perhatikan aplikasi chat yang mencatat analytics saat pesan baru masuk. Log harus menyertakan theme terkini, tetapi perubahan theme seharusnya tidak menyebabkan reconnect ke chat:
// Before useEffectEvent: useRef workaround
import { useEffect, useRef } from 'react'
export function useChatRoom(roomId: string, theme: string) {
const themeRef = useRef(theme)
themeRef.current = theme
useEffect(() => {
const connection = createConnection(roomId)
connection.on('message', (msg) => {
logAnalytics('new_message', { roomId, theme: themeRef.current })
showNotification(msg)
})
connection.connect()
return () => connection.disconnect()
}, [roomId])
}Pendekatan ini berfungsi, tetapi linter ESLint menandai theme sebagai missing dependency. Menekan warning tersebut berpotensi menyembunyikan bug di tempat lain. Pattern useRef juga mengaburkan intent: developer baru yang membaca kode ini harus menelusuri alasan mengapa theme disimpan dalam ref alih-alih dimasukkan ke dependency array.
useEffectEvent: Memisahkan Logika Reaktif dari Non-Reaktif
Hook useEffectEvent membuat fungsi stabil yang selalu membaca props dan state terbaru, tanpa memicu re-sinkronisasi effect. Hook ini menggantikan pattern useRef dengan API deklaratif yang dipahami secara native oleh linter.
// After useEffectEvent: clean separation of concerns
import { useEffect, useEffectEvent } from 'react'
export function useChatRoom(roomId: string, theme: string) {
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])
}Effect hanya berjalan ulang ketika roomId berubah. Callback onMessage selalu melihat theme terkini tanpa perlu dicantumkan sebagai dependency. Dokumentasi resmi React secara eksplisit menyatakan bahwa Effect Event hanya boleh dipanggil dari dalam effect, bukan selama rendering atau diteruskan ke komponen child.
Aturan dan Batasan useEffectEvent
Tiga aturan mengatur penggunaan useEffectEvent:
- Panggil di top level komponen atau custom hook, tidak boleh di dalam loop atau kondisi
- Fungsi yang dikembalikan hanya boleh dipanggil dari dalam
useEffectatau Effect Event lain - Tidak boleh diteruskan sebagai prop atau dikembalikan dari hook untuk konsumsi eksternal
Melanggar aturan kedua menyebabkan bug yang sulit dilacak: identitas fungsi berubah setiap render, sehingga menyimpannya dalam ref atau meneruskannya ke bawah akan mengalahkan tujuan penggunaannya. Plugin eslint-plugin-react-hooks@6+ menegakkan batasan ini secara otomatis.
import { useEffect, useEffectEvent, useState } from 'react'
interface SearchTrackerProps {
query: string
userId: string
}
export function SearchTracker({ query, userId }: SearchTrackerProps) {
const [results, setResults] = useState<string[]>([])
const onSearchComplete = useEffectEvent((resultCount: number) => {
analytics.track('search_complete', {
query,
userId,
resultCount,
timestamp: Date.now(),
})
})
useEffect(() => {
const controller = new AbortController()
fetchSearchResults(query, controller.signal).then((data) => {
setResults(data)
onSearchComplete(data.length)
})
return () => controller.abort()
}, [query])
return <ResultsList results={results} />
}useEffectEvent bukan escape hatch untuk membungkam linter dependency. Jika sebuah nilai benar-benar mengontrol kapan effect harus dijalankan ulang, nilai tersebut harus ada di dependency array. Logika hanya perlu diekstrak ke Effect Event ketika merepresentasikan aksi sampingan (logging, notifikasi, analytics) yang membaca nilai reaktif tanpa perlu memicu ulang effect.
Komponen Activity: Pre-Rendering di Background dengan Preservasi State
Komponen <Activity> (sebelumnya dikenal sebagai "Offscreen") mengontrol apakah children-nya terlihat atau tersembunyi. Berbeda dengan conditional rendering yang menghancurkan state, atau CSS display: none yang membiarkan effect tetap berjalan, <Activity> mempreservasi state sambil membersihkan effect dan menunda update.
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>
)
}Pengguna yang sedang mengisi formulir di tab "Profile" dapat berpindah ke "Settings" dan kembali tanpa kehilangan input. Effect pada tab yang tersembunyi (timer, subscription, data fetching) dibersihkan untuk membebaskan resource. Ketika tab menjadi visible kembali, effect di-mount ulang dan state dipulihkan secara instan.
Siap menguasai wawancara React / Next.js Anda?
Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.
Mode Activity: Visible vs Hidden secara Internal
Komponen <Activity> menerima prop mode dengan dua nilai:
| Perilaku | mode="visible" | mode="hidden" |
|----------|-------------------|------------------|
| Rendering DOM | Normal | display: none via CSS |
| State komponen | Aktif | Dipreservasi di memori |
| Effect (useEffect) | Ter-mount | Dibersihkan |
| Prioritas update | Normal | Ditunda ke idle |
| Pre-rendering | N/A | Render pada prioritas rendah |
Ketika komponen dimulai dalam keadaan tersembunyi (render awal dengan mode="hidden"), React melakukan pre-render pada prioritas rendah tanpa mounting effect. Mekanisme ini memungkinkan navigasi instan: halaman tujuan sudah di-render di background ketika pengguna mengklik.
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> }) {
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>
)
}<Activity> yang tersembunyi melakukan pre-render dashboard pada prioritas idle. Dikombinasikan dengan Suspense dan API use, data fetching terjadi di background. Ketika isActive berubah menjadi true, konten muncul tanpa loading spinner.
Activity dan TanStack Query: Perhatikan Cache
Kesalahan umum dengan <Activity> melibatkan TanStack Query. Karena useQuery bergantung pada useEffect secara internal, query di dalam <Activity> yang tersembunyi tidak akan dieksekusi karena effect dalam kondisi unmounted.
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { Activity } from 'react'
function UserStats() {
const { data } = useQuery({
queryKey: ['user-stats'],
queryFn: fetchUserStats,
})
return <StatsDisplay data={data} />
}
function DashboardWithPrefetch({ showStats }: { showStats: boolean }) {
const queryClient = useQueryClient()
queryClient.ensureQueryData({
queryKey: ['user-stats'],
queryFn: fetchUserStats,
})
return (
<Activity mode={showStats ? 'visible' : 'hidden'}>
<UserStats />
</Activity>
)
}Solusinya adalah memindahkan data prefetching ke komponen parent, di luar batas <Activity>. Metode ensureQueryData pada QueryClient memicu fetch jika data belum ada di cache, memastikan data tersedia secara instan ketika komponen yang tersembunyi menjadi visible.
Activity menukar memori untuk kecepatan. Setiap component tree yang tersembunyi tetap berada di memori beserta DOM lengkapnya. Untuk aplikasi dengan banyak route tersembunyi, penggunaan memori perlu dipantau secara aktif. Tim React sedang mengeksplorasi eviction otomatis untuk Activity tersembunyi yang paling jarang digunakan di rilis mendatang.
Menggabungkan useEffectEvent dan Activity
Kedua API saling melengkapi dalam pattern navigasi dunia nyata. Skenario umum: dashboard dengan tab di mana setiap tab memiliki subscription WebSocket dan analytics tracking.
import { useEffect, useEffectEvent, useState } from 'react'
import { Activity } from 'react'
function LiveFeed({ channel, userId }: { channel: string; userId: string }) {
const [messages, setMessages] = useState<Message[]>([])
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])
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>
)
}Ketika berpindah channel, WebSocket feed yang tersembunyi terputus (effect cleanup melalui <Activity>). Riwayat pesan tetap tersimpan di state. Effect Event onNewMessage memastikan analytics selalu mereferensikan userId terkini tanpa memaksa reconnect WebSocket.
Pertanyaan Interview: useEffectEvent dan Activity
Pertanyaan-pertanyaan berikut menguji pemahaman tentang API baru dan interaksinya dengan model rendering React. Pertanyaan ini semakin sering muncul dalam interview React di perusahaan yang menggunakan React 19.2.
Q1: Masalah apa yang diselesaikan useEffectEvent yang tidak bisa diselesaikan useCallback?
useCallback membuat fungsi yang di-memoize, tetapi fungsi tersebut tetap perlu dicantumkan dalam dependency array effect. Jika salah satu dependency-nya berubah, callback berubah, yang kemudian memicu ulang effect. useEffectEvent membuat fungsi yang selalu membaca nilai terbaru tanpa menjadi dependency sehingga effect tidak pernah berjalan ulang karena fungsi tersebut. Pemisahan ini tidak mungkin dicapai dengan useCallback saja.
Q2: Apakah Effect Event bisa diteruskan sebagai prop ke komponen child?
Tidak. Effect Event dirancang untuk dipanggil hanya dari dalam useEffect atau Effect Event lain. Identitasnya berubah setiap render, sehingga meneruskannya sebagai prop akan menyebabkan re-render yang tidak perlu dan merusak mental model. Plugin ESLint menegakkan aturan ini.
Q3: Bagaimana Activity berbeda dari conditional rendering dan CSS display:none?
Conditional rendering ({show && <Component />}) melakukan unmount komponen sepenuhnya sehingga state dihancurkan. CSS display: none menyembunyikan secara visual tetapi membiarkan semua effect tetap berjalan, memboroskan resource. <Activity mode="hidden"> mempreservasi state, membersihkan effect, menunda update ke prioritas idle, dan dapat melakukan pre-render konten di background.
Q4: Apa yang terjadi pada useEffect di dalam Activity yang tersembunyi?
Ketika <Activity> bertransisi ke mode="hidden", React menjalankan semua fungsi cleanup effect (nilai return dari useEffect). Tidak ada effect baru yang ter-mount selama tersembunyi. Ketika komponen menjadi visible kembali, effect di-mount ulang dengan state yang telah dipreservasi. Inilah mengapa library data fetching yang bergantung pada useEffect memerlukan strategi prefetch di luar batas Activity.
Q5: Bagaimana cara melakukan pre-render sebuah route dengan Activity dan Suspense?
Route dibungkus dalam <Activity mode="hidden"> dengan <Suspense> boundary di dalamnya. Menggunakan API use() atau sumber data yang kompatibel dengan Suspense untuk fetching, React merender tree yang tersembunyi pada prioritas rendah, menyelesaikan Suspense boundary di background. Ketika pengguna bernavigasi dan mode berubah ke "visible", konten yang sudah sepenuhnya di-render muncul secara instan tanpa loading state.
Q6: Apakah useEffectEvent menggantikan aturan exhaustive-deps pada linter?
Tidak. Aturan exhaustive-deps tetap krusial untuk mendeteksi dependency yang benar-benar terlewat. useEffectEvent menangani kasus spesifik: logika yang membaca nilai reaktif tetapi tidak boleh mengontrol kapan effect dijalankan ulang (analytics, notifikasi, logging). Menggunakannya untuk menekan semua warning dependency justru menyembunyikan bug dan bertentangan dengan tujuan penggunaannya.
Kesimpulan
useEffectEventmenggantikan workarounduseRefuntuk stale closure dalam effect, dengan dukungan linter native dieslint-plugin-react-hooks@6+- Effect Event selalu membaca props dan state terbaru tanpa memicu re-sinkronisasi effect, cocok digunakan untuk analytics, logging, dan callback notifikasi
<Activity>mempreservasi state komponen sambil membersihkan effect, menawarkan jalan tengah antara conditional rendering dan penyembunyian CSS- Activity yang tersembunyi melakukan pre-render pada prioritas idle, memungkinkan navigasi instan ketika dikombinasikan dengan
Suspensedan APIuse - TanStack Query dan library berbasis effect lainnya memerlukan prefetching di luar batas Activity karena
useEffecttidak berjalan dalam mode hidden - Kedua API tersedia di React 19.2 sehingga update ESLint dan React secara bersamaan diperlukan untuk dukungan tooling yang lengkap
Mulai berlatih!
Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.
Tag
Bagikan
Artikel terkait

Cache Components di Next.js 16 Tahun 2026: Panduan Lengkap use cache, PPR, dan Pertanyaan Interview
Panduan mendalam tentang Cache Components di Next.js 16: directive use cache di tiga cakupan, Partial Pre-Rendering, cacheLife profiles, cacheTag, keamanan cache, dan pertanyaan interview untuk developer senior tahun 2026.

React Compiler di Tahun 2026: Memoization Otomatis dan Pertanyaan Interview
Pelajari React Compiler untuk memoization otomatis, pipeline kompilasi, aturan React, dan pertanyaan interview yang sering muncul di tahun 2026.

React Server Components di produksi: pola dan jebakan
React Server Components di produksi: pola yang teruji, anti-pola umum, dan strategi debugging untuk aplikasi Next.js 15 yang andal.