# 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. - Published: 2026-06-03 - Updated: 2026-06-03 - Author: SharpSkill - Tags: react, react-19, useEffectEvent, activity, hooks, interview - Reading time: 8 min --- React 19.2 memperkenalkan dua API yang mengatasi permasalahan lama dalam ekosistem React: `useEffectEvent` menghilangkan stale closure pada effect, dan `` memungkinkan pre-rendering di background dengan preservasi state. Kedua fitur ini dirilis pada [Oktober 2025](https://react.dev/blog/2025/10/01/react-19-2) dan telah mengubah cara aplikasi React production menangani side effect dan navigasi. > **Minimum React 19.2** > > 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: ```tsx // hooks/useChatRoom.ts // 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. ```tsx // hooks/useChatRoom.ts // 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](https://react.dev/reference/react/useEffectEvent) 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`: 1. Panggil di top level komponen atau custom hook, tidak boleh di dalam loop atau kondisi 2. Fungsi yang dikembalikan hanya boleh dipanggil dari dalam `useEffect` atau Effect Event lain 3. 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](https://react.dev/reference/react/useEffectEvent) secara otomatis. ```tsx // components/SearchTracker.tsx import { useEffect, useEffectEvent, useState } from 'react' interface SearchTrackerProps { query: string userId: string } export function SearchTracker({ query, userId }: SearchTrackerProps) { const [results, setResults] = useState([]) 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 } ``` > **Kapan Tidak Menggunakan useEffectEvent** > > 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 `` (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, `` mempreservasi state sambil membersihkan effect dan menunda update. ```tsx // components/TabLayout.tsx 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 (
{tabs.map((tab) => ( {tab.content} ))}
) } ``` 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. ## Mode Activity: Visible vs Hidden secara Internal Komponen `` 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. ```tsx // components/PrerenderedRoute.tsx import { Activity, Suspense, use } from 'react' interface PrerenderedRouteProps { isActive: boolean dataPromise: Promise } export function PrerenderedRoute({ isActive, dataPromise }: PrerenderedRouteProps) { return ( }> ) } function DashboardContent({ dataPromise }: { dataPromise: Promise }) { const data = use(dataPromise) return (
) } ``` `` 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 `` melibatkan [TanStack Query](https://tanstack.com/query/latest). Karena `useQuery` bergantung pada `useEffect` secara internal, query di dalam `` yang tersembunyi tidak akan dieksekusi karena effect dalam kondisi unmounted. ```tsx // components/UserDashboard.tsx import { useQuery, useQueryClient } from '@tanstack/react-query' import { Activity } from 'react' function UserStats() { const { data } = useQuery({ queryKey: ['user-stats'], queryFn: fetchUserStats, }) return } function DashboardWithPrefetch({ showStats }: { showStats: boolean }) { const queryClient = useQueryClient() queryClient.ensureQueryData({ queryKey: ['user-stats'], queryFn: fetchUserStats, }) return ( ) } ``` Solusinya adalah memindahkan data prefetching ke komponen parent, di luar batas ``. Metode `ensureQueryData` pada QueryClient memicu fetch jika data belum ada di cache, memastikan data tersedia secara instan ketika komponen yang tersembunyi menjadi visible. > **Trade-off Memori** > > 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. ```tsx // components/LiveDashboard.tsx import { useEffect, useEffectEvent, useState } from 'react' import { Activity } from 'react' function LiveFeed({ channel, userId }: { channel: string; userId: string }) { const [messages, setMessages] = useState([]) 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 } export function LiveDashboard({ userId }: { userId: string }) { const [activeChannel, setActiveChannel] = useState('general') const channels = ['general', 'alerts', 'metrics'] return (
{channels.map((ch) => ( ))}
) } ``` Ketika berpindah channel, WebSocket feed yang tersembunyi terputus (effect cleanup melalui ``). 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](/technologies/react-next/interview-questions/react-hooks) 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](https://react.dev/reference/react/useEffectEvent). **Q3: Bagaimana Activity berbeda dari conditional rendering dan CSS display:none?** Conditional rendering (`{show && }`) melakukan unmount komponen sepenuhnya sehingga state dihancurkan. CSS `display: none` menyembunyikan secara visual tetapi membiarkan semua effect tetap berjalan, memboroskan resource. `` 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 `` 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](/technologies/react-next/interview-questions/react-query-tanstack) 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 `` dengan `` 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](https://react.dev/reference/react/useEffect) 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 - `useEffectEvent` menggantikan workaround `useRef` untuk stale closure dalam effect, dengan dukungan linter native di `eslint-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 - `` 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 `Suspense` dan API `use` - [TanStack Query](/technologies/react-next/interview-questions/react-query-tanstack) dan library berbasis effect lainnya memerlukan prefetching di luar batas Activity karena `useEffect` tidak berjalan dalam mode hidden - Kedua API tersedia di React 19.2 sehingga update ESLint dan React secara bersamaan diperlukan untuk dukungan tooling yang lengkap --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/id/blog/react-next/react-19-useeffectevent-activity-new-apis-interview-questions