title |
---|
useLayoutEffect |
useLayoutEffect
dapat memperburuk kinerja. Gunakan useEffect
bila memungkinkan.
useLayoutEffect
adalah versi useEffect
yang dijalankan sebelum peramban melukis ulang (repaint) layar.
useLayoutEffect(setup, dependencies?)
Panggil useLayoutEffect
untuk mengukur tata letak sebelum peramban melukis ulang layar:
import { useState, useRef, useLayoutEffect } from 'react';
function Tooltip() {
const ref = useRef(null);
const [tooltipHeight, setTooltipHeight] = useState(0);
useLayoutEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setTooltipHeight(height);
}, []);
// ...
Lihat contoh lainnya di bawah ini
-
setup
: Fungsi berisi logika Efek Anda. Fungsi setup juga dapat secara opsional mengembalikan fungsi pembersihan (cleanup). Sebelum komponen ditambahkan ke DOM, React akan menjalankan fungsi setup. Setelah setiap render ulang dengan dependensi yang berubah, React akan terlebih dahulu menjalankan fungsi pembersihan (jika Anda memberikannya) dengan nilai lama. Selanjutnya, React akan menjalankan fungsi setup dengan nilai baru. Sebelum komponen dihapus dari DOM, React akan menjalankan fungsi pembersihan. -
opsional
dependencies
: Daftar semua nilai reaktif yang dirujuk di dalam kodesetup
. Nilai reaktif termasuk props, state, dan semua variabel dan fungsi yang dideklarasikan langsung di dalam komponen. Jika linter Anda telah dikonfigurasi untuk React, maka linter tersebut akan memverifikasi bahwa setiap nilai reaktif sudah diatur dengan benar sebagai dependensi. Daftar dependensi ini harus memiliki jumlah item yang konstan dan ditulis secara inline seperti[dep1, dep2, dep3]
. React akan membandingkan setiap dependensi dengan nilai lama menggunakan perbandinganObject.is
. Jika argumen ini diabaikan, efek akan dijalankan ulang setelah setiap re-render dari komponen.
useLayoutEffect
mengembalikan undefined
.
-
useLayoutEffect
adalah sebuah Hook, sehingga hanya dapat memanggilnya di tingkat atas komponen ataupun di custom Hooks Anda. Serta tidak dapat dipanggil di dalam perulangan ataupun percabangan. Bila diperlukan, ekstrak komponen dan pindahkan Efek ke dalam komponen tersebut. -
Ketika Strict Mode aktif, React akan menjalankan siklus setup+pembersihan khusus pengembangan (development-only) sebelum menjalankan setup sebenarnya. Uji ketahanan (Stress-test) tersebut memastikan logika pembersihan "mencerminkan" logika setup dan pembersihan tersebut dapat menghentikan atau membatalkan apa pun yang sedang dilakukan fungsi setup. Jika hal ini menyebabkan masalah, maka implementasikan fungsi pembersihan.
-
Jika beberapa dependensi merupakan objek atau fungsi yang didefinisikan di dalam komponen, akan timbul risiko Efek dijalankan berulang kali lebih sering dari yang dibutuhkan. Untuk memperbaiki ini, hilangkan dependensi objek dan fungsi yang tidak dibutuhkan. Anda juga dapat mengekstrak pembaruan state dan logika non-reaktif diluar dari efek.
-
Efek hanya berjalan di sisi klien. Efek tidak berjalan ketika server rendering.
-
Kode di dalam
useLayoutEffect
serta semua pembaruan state yang telah dijadwalkan akan memblokir peramban untuk melukis ulang layar. Penggunaan berlebihan dapat menyebabkan aplikasi Anda lambat. Jika memungkinkan, gunakanuseEffect
. -
If you trigger a state update inside
useLayoutEffect
, React will execute all remaining Effects immediately includinguseEffect
.
Mengukur tata letak sebelum peramban melukis ulang layar {/measuring-layout-before-the-browser-repaints-the-screen/}
Sebagian besar komponen tidak perlu mengetahui posisi dan ukuran di layar untuk memutuskan apa yang harus di-render. Komponen hanya mengembalikan beberapa JSX. Selanjutnya, peramban akan mengukur tata letak (posisi dan ukuran) dan melukis ulang layar.
Terkadang, itu tidak cukup. Bayangkan sebuah tooltip berada di sebelah elemen tertentu saat diarahkan (hover). Jika ruang mencukupi, posisi tooltip harus berada di atas elemen tersebut. Tetapi, jika tidak cukup, posisi tooltip harus berada di bawah. Untuk me-render tooltip di posisi akhir yang tepat, maka Anda harus mengetahui ketinggiannya (yaitu, apakah muat berada di atas).
Untuk melakukan hal tersebut, Anda perlu me-render dalam dua tahap:
- Me-render tooltip di mana saja (bahkan dengan posisi yang salah).
- Mengukur tingginya dan menentukan posisi tooltip tersebut.
- Me-render ulang tooltip agar berada di posisi yang tepat.
Seluruh proses tersebut harus terjadi sebelum peramban melukis ulang layar. Pengguna tidak ingin melihat tooltip bergerak. Panggil useLayoutEffect
untuk mengukur tata letak sebelum peramban melukis ulang layar:
function Tooltip() {
const ref = useRef(null);
const [tooltipHeight, setTooltipHeight] = useState(0); // Belum mengetahui ketinggian tooltip sebenarnya
useLayoutEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setTooltipHeight(height); // Lakukan re-render setelah mengetahui ketinggian tooltip
}, []);
// ...gunakan tooltipHeight pada logika render di bawah ini ...
}
Berikut adalah langkah-langkah cara kerja:
Tooltip
di-render dengan menginisialisasi nilaitooltipHeight = 0
(Sehingga, memungkinkan tooltip berada di posisi yang salah).- React menempatkannya di DOM dan menjalankan kode di
useLayoutEffect
. useLayoutEffect
mengukur tinggi kontentooltip
dan akan segera memicu render ulang.Tooltip
di-render ulang dengan nilaitooltipHeight
yang sebenarnya (sehingga tooltip berada di posisi yang benar).- React memperbarui DOM dan akhirnya peramban menampilkan tooltip tersebut.
Arahkan kursor ke tombol berikut dan perhatikan tooltip menyesuaikan posisinya tergantung dari ketersediaan ruang:
import ButtonWithTooltip from './ButtonWithTooltip.js';
export default function App() {
return (
<div>
<ButtonWithTooltip
tooltipContent={
<div>
Tooltip ini tidak muat di atas tombol
<br />
Ini sebabnya tooltip ditampilkan di bawah
</div>
}
>
Arahkan disini (tooltip berada di atas)
</ButtonWithTooltip>
<div style={{ height: 50 }} />
<ButtonWithTooltip
tooltipContent={
<div>Tooltip ini muat di atas tombol</div>
}
>
Arahkan disini (tooltip berada di bawah)
</ButtonWithTooltip>
<div style={{ height: 50 }} />
<ButtonWithTooltip
tooltipContent={
<div>Tooltip ini muat di atas tombol</div>
}
>
Arahkan disini (tooltip berada di bawah)
</ButtonWithTooltip>
</div>
);
}
import { useState, useRef } from 'react';
import Tooltip from './Tooltip.js';
export default function ButtonWithTooltip({ tooltipContent, ...rest }) {
const [targetRect, setTargetRect] = useState(null);
const buttonRef = useRef(null);
return (
<>
<button
{...rest}
ref={buttonRef}
onPointerEnter={() => {
const rect = buttonRef.current.getBoundingClientRect();
setTargetRect({
left: rect.left,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
});
}}
onPointerLeave={() => {
setTargetRect(null);
}}
/>
{targetRect !== null && (
<Tooltip targetRect={targetRect}>
{tooltipContent}
</Tooltip>
)
}
</>
);
}
import { useRef, useLayoutEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import TooltipContainer from './TooltipContainer.js';
export default function Tooltip({ children, targetRect }) {
const ref = useRef(null);
const [tooltipHeight, setTooltipHeight] = useState(0);
useLayoutEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setTooltipHeight(height);
console.log('Hasil pengukuran tinggi tooltip: ' + height);
}, []);
let tooltipX = 0;
let tooltipY = 0;
if (targetRect !== null) {
tooltipX = targetRect.left;
tooltipY = targetRect.top - tooltipHeight;
if (tooltipY < 0) {
// Tooltip tidak muat di atas, maka diletakkan di bawah
tooltipY = targetRect.bottom;
}
}
return createPortal(
<TooltipContainer x={tooltipX} y={tooltipY} contentRef={ref}>
{children}
</TooltipContainer>,
document.body
);
}
export default function TooltipContainer({ children, x, y, contentRef }) {
return (
<div
style={{
position: 'absolute',
pointerEvents: 'none',
left: 0,
top: 0,
transform: `translate3d(${x}px, ${y}px, 0)`
}}
>
<div ref={contentRef} className="tooltip">
{children}
</div>
</div>
);
}
.tooltip {
color: white;
background: #222;
border-radius: 4px;
padding: 4px;
}
Meskipun komponen Tooltip
harus di-render dalam dua tahap (pertama, dengan nilai tooltipHeight
diinisialisasi 0
dan ketika nilai tersebut diukur sesuai dengan tinggi sebenarnya), Anda hanya melihat hasil akhirnya. Ini sebabnya mengapa Anda menggunakan useLayoutEffect
dibandingkan useEffect
untuk kasus contoh tersebut. Mari kita lihat perbedaanya secara detail di bawah ini.
useLayoutEffect
memblokir peramban untuk melukis ulang {/uselayouteffect-blocks-the-browser-from-repainting/}
React menjamin kode di dalam useLayoutEffect
dan setiap pembaruan state yang dijadwalkan akan diproses sebelum peramban melukis ulang layar. Hal ini memungkinkan Anda untuk me-render tooltip, mengukurnya, dan me-render ulang kembali tooltip tersebut tanpa pengguna menyadari render awal tambahan. Dengan kata lain, useLayoutEffect
memblokir peramban untuk melukis ulang.
import ButtonWithTooltip from './ButtonWithTooltip.js';
export default function App() {
return (
<div>
<ButtonWithTooltip
tooltipContent={
<div>
Tooltip ini tidak muat di atas tombol
<br />
Ini sebabnya tooltip ditampilkan di bawah
</div>
}
>
Arahkan disini (tooltip berada di atas)
</ButtonWithTooltip>
<div style={{ height: 50 }} />
<ButtonWithTooltip
tooltipContent={
<div>Tooltip ini muat di atas tombol</div>
}
>
Arahkan disini (tooltip berada di bawah)
</ButtonWithTooltip>
<div style={{ height: 50 }} />
<ButtonWithTooltip
tooltipContent={
<div>Tooltip ini muat di atas tombol</div>
}
>
Arahkan disini (tooltip berada di bawah)
</ButtonWithTooltip>
</div>
);
}
import { useState, useRef } from 'react';
import Tooltip from './Tooltip.js';
export default function ButtonWithTooltip({ tooltipContent, ...rest }) {
const [targetRect, setTargetRect] = useState(null);
const buttonRef = useRef(null);
return (
<>
<button
{...rest}
ref={buttonRef}
onPointerEnter={() => {
const rect = buttonRef.current.getBoundingClientRect();
setTargetRect({
left: rect.left,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
});
}}
onPointerLeave={() => {
setTargetRect(null);
}}
/>
{targetRect !== null && (
<Tooltip targetRect={targetRect}>
{tooltipContent}
</Tooltip>
)
}
</>
);
}
import { useRef, useLayoutEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import TooltipContainer from './TooltipContainer.js';
export default function Tooltip({ children, targetRect }) {
const ref = useRef(null);
const [tooltipHeight, setTooltipHeight] = useState(0);
useLayoutEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setTooltipHeight(height);
}, []);
let tooltipX = 0;
let tooltipY = 0;
if (targetRect !== null) {
tooltipX = targetRect.left;
tooltipY = targetRect.top - tooltipHeight;
if (tooltipY < 0) {
// Tooltip tidak muat di atas, maka diletakkan di bawah
tooltipY = targetRect.bottom;
}
}
return createPortal(
<TooltipContainer x={tooltipX} y={tooltipY} contentRef={ref}>
{children}
</TooltipContainer>,
document.body
);
}
export default function TooltipContainer({ children, x, y, contentRef }) {
return (
<div
style={{
position: 'absolute',
pointerEvents: 'none',
left: 0,
top: 0,
transform: `translate3d(${x}px, ${y}px, 0)`
}}
>
<div ref={contentRef} className="tooltip">
{children}
</div>
</div>
);
}
.tooltip {
color: white;
background: #222;
border-radius: 4px;
padding: 4px;
}
Berikut merupakan contoh identik menggunakan useEffect
daripada useLayoutEffect
. Jika menggunakan perangkat yang lebih lambat, terkadang tooltip terlihat "berkedip" dan secara singkat posisi awal tooltip terlihat sebelum diperbaiki.
import ButtonWithTooltip from './ButtonWithTooltip.js';
export default function App() {
return (
<div>
<ButtonWithTooltip
tooltipContent={
<div>
Tooltip ini tidak muat di atas tombol
<br />
Ini sebabnya tooltip ditampilkan di bawah
</div>
}
>
Arahkan disini (tooltip berada di atas)
</ButtonWithTooltip>
<div style={{ height: 50 }} />
<ButtonWithTooltip
tooltipContent={
<div>Tooltip ini muat di atas tombol</div>
}
>
Arahkan disini (tooltip berada di bawah)
</ButtonWithTooltip>
<div style={{ height: 50 }} />
<ButtonWithTooltip
tooltipContent={
<div>Tooltip ini muat di atas tombol</div>
}
>
Arahkan disini (tooltip berada di bawah)
</ButtonWithTooltip>
</div>
);
}
import { useState, useRef } from 'react';
import Tooltip from './Tooltip.js';
export default function ButtonWithTooltip({ tooltipContent, ...rest }) {
const [targetRect, setTargetRect] = useState(null);
const buttonRef = useRef(null);
return (
<>
<button
{...rest}
ref={buttonRef}
onPointerEnter={() => {
const rect = buttonRef.current.getBoundingClientRect();
setTargetRect({
left: rect.left,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
});
}}
onPointerLeave={() => {
setTargetRect(null);
}}
/>
{targetRect !== null && (
<Tooltip targetRect={targetRect}>
{tooltipContent}
</Tooltip>
)
}
</>
);
}
import { useRef, useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import TooltipContainer from './TooltipContainer.js';
export default function Tooltip({ children, targetRect }) {
const ref = useRef(null);
const [tooltipHeight, setTooltipHeight] = useState(0);
useEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setTooltipHeight(height);
}, []);
let tooltipX = 0;
let tooltipY = 0;
if (targetRect !== null) {
tooltipX = targetRect.left;
tooltipY = targetRect.top - tooltipHeight;
if (tooltipY < 0) {
// Tooltip tidak muat di atas, maka diletakkan di bawah
tooltipY = targetRect.bottom;
}
}
return createPortal(
<TooltipContainer x={tooltipX} y={tooltipY} contentRef={ref}>
{children}
</TooltipContainer>,
document.body
);
}
export default function TooltipContainer({ children, x, y, contentRef }) {
return (
<div
style={{
position: 'absolute',
pointerEvents: 'none',
left: 0,
top: 0,
transform: `translate3d(${x}px, ${y}px, 0)`
}}
>
<div ref={contentRef} className="tooltip">
{children}
</div>
</div>
);
}
.tooltip {
color: white;
background: #222;
border-radius: 4px;
padding: 4px;
}
Untuk mempermudah reproduski bug ini, versi berikut menambahkan penundaan buatan saat me-render. React akan membiarkan peramban melukis ulang layar sebelum memproses pembaruan state di dalam useEffect
. Hasilnya, tooltip berkedip:
import ButtonWithTooltip from './ButtonWithTooltip.js';
export default function App() {
return (
<div>
<ButtonWithTooltip
tooltipContent={
<div>
Tooltip ini tidak muat di atas tombol
<br />
Ini sebabnya tooltip ditampilkan di bawah
</div>
}
>
Arahkan disini (tooltip berada di atas)
</ButtonWithTooltip>
<div style={{ height: 50 }} />
<ButtonWithTooltip
tooltipContent={
<div>Tooltip ini muat di atas tombol</div>
}
>
Arahkan disini (tooltip berada di bawah)
</ButtonWithTooltip>
<div style={{ height: 50 }} />
<ButtonWithTooltip
tooltipContent={
<div>Tooltip ini muat di atas tombol</div>
}
>
Arahkan disini (tooltip berada di bawah)
</ButtonWithTooltip>
</div>
);
}
import { useState, useRef } from 'react';
import Tooltip from './Tooltip.js';
export default function ButtonWithTooltip({ tooltipContent, ...rest }) {
const [targetRect, setTargetRect] = useState(null);
const buttonRef = useRef(null);
return (
<>
<button
{...rest}
ref={buttonRef}
onPointerEnter={() => {
const rect = buttonRef.current.getBoundingClientRect();
setTargetRect({
left: rect.left,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
});
}}
onPointerLeave={() => {
setTargetRect(null);
}}
/>
{targetRect !== null && (
<Tooltip targetRect={targetRect}>
{tooltipContent}
</Tooltip>
)
}
</>
);
}
import { useRef, useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import TooltipContainer from './TooltipContainer.js';
export default function Tooltip({ children, targetRect }) {
const ref = useRef(null);
const [tooltipHeight, setTooltipHeight] = useState(0);
// Berikut secara artifisial memperlambat proses me-*render*
let now = performance.now();
while (performance.now() - now < 100) {
// Sementara tidak melakukan apa pun...
}
useEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setTooltipHeight(height);
}, []);
let tooltipX = 0;
let tooltipY = 0;
if (targetRect !== null) {
tooltipX = targetRect.left;
tooltipY = targetRect.top - tooltipHeight;
if (tooltipY < 0) {
// Tooltip tidak muat di atas, maka diletakkan di bawah
tooltipY = targetRect.bottom;
}
}
return createPortal(
<TooltipContainer x={tooltipX} y={tooltipY} contentRef={ref}>
{children}
</TooltipContainer>,
document.body
);
}
export default function TooltipContainer({ children, x, y, contentRef }) {
return (
<div
style={{
position: 'absolute',
pointerEvents: 'none',
left: 0,
top: 0,
transform: `translate3d(${x}px, ${y}px, 0)`
}}
>
<div ref={contentRef} className="tooltip">
{children}
</div>
</div>
);
}
.tooltip {
color: white;
background: #222;
border-radius: 4px;
padding: 4px;
}
Ubah contoh berikut menjadi useLayoutEffect
kemudian amati proses melukis layar akan terhalang meskipun proses me-render diperlambat.
Proses me-render dua tahap dan memblokir peramban akan menurunkan kinerja. Sebaiknya proses tersebut dihindari.
Saya menerima error: "useLayoutEffect
does nothing on the server" {/im-getting-an-error-uselayouteffect-does-nothing-on-the-server/}
Tujuan dari useLayoutEffect
adalah memungkinkan sebuah komponen menggunakan informasi tata letak untuk me-render:
- Me-render konten awal.
- Mengukur tata letak sebelum peramban melukis ulang layar.
- Me-render konten akhir menggunakan informasi tata letak yang telah dibaca.
Ketika Anda atau framework Anda menggunakan server rendering, aplikasi React Anda di-render menjadi HTML di server saat awal me-render.
Masalahnya, di server tidak tersedia informasi tentang tata letak.
Pada contoh sebelumnya, pemanggilan useLayoutEffect
pada komponen Tooltip
memungkinkan posisi tooltip disesuaikan dengan benar (antara di atas atau di bawah konten) tergantung pada ketinggian konten. Sedangkan, jika Anda mencoba me-render Tooltip
sebagai bagian dari HTML server awal, hal tersebut tidak mungkin dapat dilakukan. Sebab di server belum terdapat tata letak! Jadi, meskipun Anda me-rendernya di server, posisi tooltip akan "melompat" di sisi klien setelah JavaScript dimuat dan dijalankan.
Biasanya, komponen yang bergantung pada informasi tata letak tidak perlu di-render di server. Sebagai contoh, tidak akan masuk akal untuk menampilkan Tooltip
selama render awal. Karena hal tersebut dipicu oleh interaksi klien.
Namun, jika mengalami masalah tersebut, terdapat beberapa opsi yang tersedia:
-
Ubah
useLayoutEffect
menjadiuseEffect
. Hal tersebut menginformasikan React bahwa hasil render awal dapat ditampilkan tanpa memblokir proses melukis (karena HTML asli akan terlihat sebelum Efek dijalankan). -
Sebagai alternatif, tandai komponen Anda sebagai khusus klien (client-only). Hal tersebut menginformasikan React untuk mengganti kontennya hingga
<Suspense>
terdekat dengan fallback loading (Sebagai contoh, spinner atau glimmer) selama server rendering. -
Sebagai alternatif, Anda dapat me-render komponen dengan
useLayoutEffect
hanya setelah proses hidrasi (hydration). Tambahkan state booleanisMounted
yang diinisialisasi dengan nilaifalse
, dan atur nilainya menjaditrue
di dalam panggilanuseEffect
. Logika me-render Anda dapat dituliskan seperti ini:return isMounted ? <RealContent /> : <FallbackContent />
. Selama di sisi server atau proses hidrasi, pengguna akan melihatFallbackContent
yang tidak memanggiluseLayoutEffect
. Kemudian React akan menggantinya denganRealContent
yang hanya dijalankan di sisi klien dan juga dapat mencakup pemanggilanuseLayoutEffect
. -
Jika Anda menyinkronkan komponen dengan penyimpanan data eksternal menggunakan
useLayoutEffect
untuk penggunaan selain dari pengukuran tata letak, pertimbangkanuseSyncExternalStore
yang mendukung server rendering sebagai gantinya.