Pernahkah Anda membuat tooltip yang tiba-tiba berpindah dari sudut kiri atas ke posisi yang benar? Atau modal yang berkedip dengan ukuran yang salah sebelum akhirnya menetap di tempatnya? Glitch sepersekian detik itu adalah layout flicker. Hal ini terjadi ketika React membaca DOM, menghitung koreksi, dan memperbarui state, tetapi browser sudah mulai menampilkan piksel ke layar. Solusi yang biasanya disarankan adalah mengganti useEffect dengan useLayoutEffect. Perubahan ini berhasil, tetapi hanya jika Anda memahami dengan tepat kapan setiap hook dijalankan di dalam pipeline browser.

Pipeline Browser: Render, Commit, Paint

React memperbarui komponen dalam tiga tahap yang berbeda. Pada fase render, React membangun—atau membangun ulang—Virtual DOM dan menghitung diff. Belum ada perubahan piksel yang nyata; ini murni perhitungan yang terjadi di dalam memori. Selanjutnya adalah fase commit, di mana React menerapkan perubahan tersebut ke node DOM yang sebenarnya. Gaya (styles) diperbarui, node dimasukkan atau dihapus, dan teks berubah.

Kemudian browser mengambil alih. Pada fase paint, mesin perenderan browser menghitung geometri tata letak (layout) dan menggambar piksel ke layar. Urutan ini bersifat kaku. Browser harus menyelesaikan layout sebelum dapat melakukan paint, dan harus menyelesaikan paint sebelum pengguna melihat sesuatu yang baru. Celah antara commit dan paint diukur dalam milidetik, tetapi celah itu nyata, dan itulah jendela di mana useEffect dan useLayoutEffect berbeda.

Mengapa useEffect Menyebabkan Kedipan

useEffect berjalan secara asinkron, dijadwalkan untuk dijalankan setelah browser selesai melakukan paint pada layar. DOM diperbarui, piksel digambar, dan kemudian React masuk kembali untuk menjalankan effect Anda.

Bayangkan Anda merender menu dropdown di bawah sebuah tombol. Di dalam useEffect, Anda memanggil buttonRef.current.getBoundingClientRect(), menghitung koordinat top dan left yang benar, lalu menyimpannya ke dalam state. Karena useEffect berjalan setelah paint, browser sudah menggambar dropdown pada posisi default-nya, mungkin di top: 0, left: 0. Baru setelah paint tersebut, effect Anda memperbarui state. React melakukan commit pada koordinat yang telah dikoreksi, dan browser melakukan paint lagi. Pengguna melihat dua frame: posisi yang salah, lalu posisi yang benar. Lonjakan visual itulah flicker yang coba dihindari semua orang.

Untuk pengambilan data (data fetching), panggilan API, pelacakan analitik, atau pengaturan event listener, penundaan ini tidak menjadi masalah. Pengguna tidak peduli apakah sinyal analitik menyala beberapa milidetik setelah paint. Faktanya, menunda pekerjaan non-visual hingga setelah paint menjaga render awal tetap responsif. Namun untuk koreksi yang bergantung pada tata letak (layout), useEffect sudah terlambat.

Bagaimana useLayoutEffect Memblokir Paint

useLayoutEffect berjalan secara sinkron, segera setelah React mengubah (mutate) DOM tetapi sebelum browser memiliki kesempatan untuk menghitung layout atau menggambar piksel. Ini memblokir seluruh pipeline paint.

Jika Anda melakukan pengukuran dropdown yang sama di dalam useLayoutEffect, urutannya berubah. React melakukan commit pada pembaruan DOM awal, menjalankan layout effect Anda, dan pembaruan state Anda memicu re-render sinkron. React melakukan commit pada koordinat yang telah dikoreksi, dan baru setelah itu browser melakukan paint. Pengguna hanya melihat satu frame, yang sudah benar.

Perilaku pemblokiran tersebut adalah fitur sekaligus risiko. Karena useLayoutEffect mencegah browser melakukan paint sampai ia selesai, komputasi berat apa pun di dalamnya akan membekukan UI. Bahkan beberapa puluh milidetik paint yang terblokir akan terasa seperti jank bagi pengguna. Inilah sebabnya mengapa dokumentasi React secara eksplisit menyarankan Anda untuk memulai dengan useEffect dan hanya beralih ke useLayoutEffect ketika Anda benar-benar melihat kedipan yang tidak dapat ditoleransi.

Kapan Menggunakan Masing-masing Hook

Sebagian besar logika Anda seharusnya berada di dalam useEffect. Gunakan untuk:

  • Mengambil data dari API
  • Mengatur subscriptions atau event listeners
  • Mengirim peristiwa (events) analitik
  • Efek samping (side effect) apa pun yang tidak membaca atau mengubah tata letak secara langsung

Simpan useLayoutEffect untuk operasi yang harus membaca DOM dan menulis kembali sebelum pengguna melihat frame:

  • Mengukur dimensi elemen, seperti lebar, tinggi, atau posisi gulir (scroll)
  • Menghitung koordinat untuk tooltips, popovers, atau menu konteks
  • Mencegah pergeseran tata letak (layout shifts) yang terlihat ketika posisi visual bergantung pada geometri yang dirender

Jika Anda tidak yakin mana yang harus dipilih, gunakan useEffect sebagai standar. Beralihlah ke useLayoutEffect hanya jika Anda menyadari adanya ketidakstabilan visual. Aturan ini saja sudah cukup untuk menjaga sebagian besar aplikasi React berjalan dengan lancar.

Masalah Server-Side Rendering

Jika Anda menggunakan Next.js, Remix, atau framework apa pun yang merender React di server, Anda akan menemui peringatan dengan useLayoutEffect. Karena server tidak memiliki DOM, hook tersebut tidak memiliki apa pun untuk diukur. React memperingatkan Anda bahwa ia mengharapkan lingkungan browser tetapi tidak menemukannya. Selama proses hidrasi, ketidakcocokan ini juga dapat menyebabkan bug yang samar karena markup yang dirender di server dan render pertama yang dimaksudkan oleh klien mungkin berbeda.

Solusi standarnya adalah hook isomorfik yang memilih effect yang tepat berdasarkan lingkungannya:

const useIsomorphicLayoutEffect =
  typeof window !== 'undefined' ? useLayoutEffect : useEffect;

Gunakan wrapper ini di komponen apa pun yang harus mengukur node DOM tetapi mungkin dieksekusi selama perenderan server. Ini akan membungkam peringatan tersebut dan menjaga output server Anda tetap konsisten.

Performa dan Praktik Terbaik

Karena useLayoutEffect memblokir proses painting, jaga isi hook agar seringan mungkin. Baca nilai layout, hitung koreksinya, dan tulis kembali. Jangan melakukan fetch data, memproses objek besar, atau menjalankan algoritma yang berat di dalamnya. Kode yang berat di sini akan menghentikan main thread dan membuat antarmuka Anda terasa membeku.

Saat Anda mengukur elemen, gunakan React refs daripada document.getElementById. Refs terikat pada instance komponen Anda, bertahan melalui re-render tanpa trik query, dan bekerja secara andal dengan portal atau perenderan kondisional. Pencarian ID global merusak enkapsulasi komponen dan dapat mengembalikan null tepat pada saat Anda membutuhkannya.

useEffect adalah pilihan default yang tepat untuk hampir setiap side effect. Ini memungkinkan browser melakukan painting tanpa gangguan dan menangani data, event, serta sinkronisasi eksternal dengan bersih. useLayoutEffect adalah alat khusus untuk masalah tertentu: membaca layout dan menulisnya kembali sebelum proses painting. Kuasai perbedaan waktu di antara keduanya, dan Anda akan berhenti mengejar flicker dan mulai mencegahnya.

Poin pentingnya: Mulailah dengan useEffect untuk segalanya. Saat Anda melihat tooltip atau modal berkedip ke tempat yang salah sebelum memperbaiki dirinya sendiri, itulah sinyal Anda. Beralihlah ke useLayoutEffect, ukur DOM, sesuaikan layout Anda, dan biarkan browser melakukan painting satu kali—dengan benar.