useLayoutEffect

Tuzak

useLayoutEffect performansı kötü etkileyebilir. Mümkün olduğunca useEffect’i tercih edin.

useLayoutEffect, tarayıcı bileşeni ekrana çizmeden önce tetiklenen bir useEffect çeşididir.

useLayoutEffect(setup, dependencies?)

Referans

useLayoutEffect(setup, dependencies?)

Yerleşim (layout) ölçümlerini gerçekleştirmek için, useLayoutEffect’i tarayıcının ekrana yeniden çizme işleminden (repainting) önce çağırın:

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);
}, []);
// ...

Daha fazla örnek için aşağıya bakın.

Parametreler

  • kurulum (setup): Efekt kodunu barındıran fonksiyondur. Kurulum fonksiyonunuz ayrıca isteğe bağlı temizleme (cleanup) fonksiyonu döndürebilir. React, bileşeninizi DOM’a eklemeden önce kurulum fonksiyonunuzu çalıştırır. Bağımlılıkların değişmesiyle tetiklenen render’ların ardından öncelikle eski değerlerle birlikte temizleme fonksiyonunuzu (varsa) çalıştırılır ve ardından yeni değerlerle birlikte kurulum fonksiyonuzu çalıştırılır. Bileşen DOM’dan kaldırılmadan önce, React son kez temizleme fonksiyonunuzu çalıştırır.

  • isteğe bağlı bağımlılıklar (dependencies): Kurulum fonksiyonu içerisinde başvurulan reaktif değerlerin listesidir. Reaktif değerler; prop’lar, state’ler ve de bileşenin içerisinde tanımlanan değişkenler ve fonksiyonlardır. Linter’ınız React için yapılandırılmışsa, her reaktif değerin doğru bir şekilde bağımlılıklara eklendiğini kontrol eder. Bağımlılık listesi, sabit sayıda öğeye sahiptir ve [dep1, dep2, dep3] şeklinde satır içi yazılmalıdır. React, Object.is kullanarak bağımlılıkları önceki değerleriyle karşılaştırır. Eğer bu argümanı atlarsanız, bileşen her render edildiğinde efekt çalıştırılır.

Dönüş değeri

useLayoutEffect, undefined döndürür.

Dikkat edilmesi gerekenler

  • useLayoutEffect bir Hook olduğundan, yalnızca bileşeninizin en üst kapsamında ya da kendi Hook’larınızda çağırabilirsiniz. Döngülerin ve koşulların içinde çağıramazsınız. Eğer çağırmak zorunda kaldıysanız yeni bir bileşene çıkarın ve efekti oraya taşıyın.

  • Katı Mod (Strict Mode) açık olduğunda React, ilk kurulumdan önce yalnızca geliştirme ortamında olmak üzere ekstra bir kurulum+temizleme döngüsü çalıştırır. Bu, temizleme mantığının kurulum mantığını “yansıtmasını” ve kurulumun yaptığı her şeyi durdurmasını veya geri almasını sağlayan bir stres testidir. Eğer bir probleme neden olursa temizleme fonksiyonunu implemente edin.

  • Bileşen içerisinde tanımlanıp bağımlılıklarınıza eklenmiş bazı nesneler ve fonksiyonlar, efektin gereksiz yere yeniden çalışmasına neden olabilir. Bu sorunu çözmek için gereksiz nesne ve fonksiyon bağımlılıklarını kaldırabilirsiniz. Ayrıca state güncellemelerini ve reaktif olmayan kodları efekt dışına çıkarabilirsiniz.

  • Efektler yalnızca istemcide (client) çalışır. Sunucu taraflı renderlama (server-side rendering) esnasında çalışmaz.

  • useLayoutEffect fonksiyonunun kodu ve içerisinde planlanan state güncellemeleri, tarayıcının ekrana yeniden çizme işlemini bloklar. Aynı zamanda gereğinden fazla kullanılması durumunda uygulamanızı yavaşlatabilir. Mümkün olduğunca useEffect’i tercih edin.

  • If you trigger a state update inside useLayoutEffect, React will execute all remaining Effects immediately including useEffect.


Kullanım

Tarayıcı ekrana çizmeden önce yerleşimi ölçmek

Çoğu bileşen, render edeceği elementlere karar vermek için ekrandaki konumlarını ve boyutlarını bilmeye ihtiyaç duymaz. Sadece JSX döndürürler. Sonrasında tarayıcı, yerleşimlerini (konum ve boyut) hesaplar ve ekrana çizer.

Bazen bu yeterli olmaz. Bir elementin üzerine gelindiğinde yanında görünen bir ipucu kutusunu (tooltip) düşünün. Yeterli alan mevcutsa öğenin üstünde, yoksa altında görünmelidir. Doğru konumda render edilmesi için kutunun yüksekliğini bilmeye ihtiyacınız (üstteki boşluğa sığdığından emin olmak adına) vardır.

Bunu yapmak için çift dikiş renderlamaya ihtiyacınız vardır:

  1. İpucunu herhangi bir yerde render edin (konumu farketmez).
  2. Yüksekliğini ölçün ve ipucunu yerleştireceğiniz yere karar verin.
  3. İpucunu doğru yerde yeniden render edin.

Tüm bunlar tarayıcı ekrana yeniden çizmeden önce yaşanmalıdır. Kullanıcının kutunun konum değiştirme hareketini görmesini istemezsiniz. Yerleşim ölçümlerini yapmak için useLayoutEffect’i çağırın:

function Tooltip() {
const ref = useRef(null);
const [tooltipHeight, setTooltipHeight] = useState(0); // Henüz yüksekliğini bilmiyorsunuz

useLayoutEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setTooltipHeight(height); // Gerçek yüksekliği öğrendikten sonra yeniden render edin
}, []);

// ...aşağıda bulunan rendering mantığında tooltipHeight'i kullanın...
}

İşleyişi adım adım şu şekilde açıklanabilir:

  1. Tooltip, başlangıçta tooltipHeight = 0 olarak render edilir (kutu yanlış konumlandırılabilir).
  2. React, DOM’a yerleştirir ve useLayoutEffect’teki kodu çalıştırır.
  3. useLayoutEffect, ipucu kutusunun yüksekliğini ölçer ve anında yeniden render’ı tetikler.
  4. Tooltip, gerçek tooltipHeight ile yeniden render edilir (kutu doğru konumlandırılır).
  5. React, DOM’u günceller ve tarayıcı ipucu kutusunu nihayet görüntüler.

Aşağıdaki düğmelerin üzerine gelip ipucu kutusunun sığıp sığmadığına bağlı olarak nasıl konumlandırılacağını görün:

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('Ölçülen ipucu kutusu yüksekliği: ' + height);
  }, []);

  let tooltipX = 0;
  let tooltipY = 0;
  if (targetRect !== null) {
    tooltipX = targetRect.left;
    tooltipY = targetRect.top - tooltipHeight;
    if (tooltipY < 0) {
      // Yukarıya sığmadığı için altına yerleştirilir
      tooltipY = targetRect.bottom;
    }
  }

  return createPortal(
    <TooltipContainer x={tooltipX} y={tooltipY} contentRef={ref}>
      {children}
    </TooltipContainer>,
    document.body
  );
}

Dikkat edin, Tooltip bileşeni çift dikiş (önce tooltipHeight = 0 olarak, ardından ölçülen yükseklikle) render edilmesine rağmen, yalnızca nihai sonucu görürsünüz. Elbette bunun için örnekteki gibi useEffect yerine useLayoutEffect kullanmanız gerekir. Farkı detaylıca inceleyelim.

useLayoutEffect vs useEffect

Örnek 1 / 2:
useLayoutEffect tarayıcının ekrana çizme işlemini bloklar

React; useLayoutEffect kodunun ve içerisinde planlanan durum güncellemelerinin, tarayıcı ekranı yeniden çizmeden önce işleme alınacağının garantisini verir. Bu da kullanıcı ilk render’ı fark edemeden tooltip’i ölçmenize ve tekrar render etmenize olanak tanır. Başka bir deyişle, useLayoutEffect tarayıcının ekrana çizmesini engeller.

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) {
      // Yukarıya sığmadığı için altına yerleştirilir
      tooltipY = targetRect.bottom;
    }
  }

  return createPortal(
    <TooltipContainer x={tooltipX} y={tooltipY} contentRef={ref}>
      {children}
    </TooltipContainer>,
    document.body
  );
}

Not

İki aşamalı render’lama ve tarayıcıyı bloklama performansı düşürür. Yapabildiğinizce bundan kaçınmaya çalışın.


Sorun giderme

useLayoutEffect does nothing on the server” hatası alıyorum

useLayoutEffect’in amacı, bileşeninizin render anında yerleşim bilgisini kullanmasına izin vermektir:

  1. Başlangıç içeriğini render edin.
  2. Tarayıcı ekranı çizmeden önce yerleşimi ölçün.
  3. Okuduğunuz yerleşim bilgisini kullanarak nihai içeriği render edin.

Siz veya framework’ünüz sunucu taraflı render’lama kullanıyorsa, React uygulamanız ilk render için sunucuda HTML render eder. Bu da JavaScript kodu yüklenmeden önce HTML’ini göstermenize sebep olur.

Sorunun kaynağı sunucuda yerleşim (layout) bilgisinin bulunmamasıdır.

Önceki örneklerde bahsedilen Tooltip bileşenindeki useLayoutEffect çağrısı, içeriğin yüksekliğine bağlı olarak doğru konuma yerleşmesine izin veriyordu (içeriğin üstünde veya altında). Tooltip’i sunucu HTML’inin bir parçası olarak render etmeye çalışırsanız, konumu belirlemek imkansız olur. Sunucuda yerleşim yoktur! Bu nedenle, sunucuda render edilen bileşeniniz JavaScript yüklenip çalıştırıldıktan sonra nihai konumuna “zıplar”.

Genellikle yerleşim bilgisine ihtiyaç duyan bileşenlerin sunucuda render edilmesi gereksizdir. Örneğin, ilk render’da Tooltip göstermek çoğu zaman mantıklı değildir. İstemci etkileşimiyle tetiklenmelidir.

Bununla birlikte bu sorunla karşılaştığınızda seçebileceğiniz birkaç farklı seçenek vardır:

  • useLayoutEffect’i useEffect ile değiştirin. Bu, React’a ekrana çizme işlemini bloke etmesine gerek olmadan ilk render sonucunu görüntüleyebileceğini söyler (çünkü efektiniz çalışmadan önce HTML render edilmiş olacaktır).

  • Alternatif olarak, bileşeninizi yalnızca istemci taraflı render olacak şekilde işaretleyin. Böylece bileşen sunucu tarafında render edilirken en yakındaki <Suspense> sınırına (boundary) yüklenme fallback’i olarak verilen bileşen (örneğin, spinner veya glimmer) ile değiştirilir.

  • Alternatif olarak, useLayoutEffect kullanan bileşeni hidratlama sonrasında render ettirebilirsiniz. Başlangıç değeri false olan isMounted isminde bir state oluşturun ve useEffect içerisinde true olarak ayarlayın. Render ederken lojiğiniz şöyle olabilir: return isMounted ? <RealContent /> : <FallbackContent />. Sunucudayken veya hidratlama sırasında, kullanıcı useLayoutEffect çağırılmayan FallbackContent’i görür. İstemci tarafında React, içeriği RealContent ile değiştirir.

  • Bileşeninizi harici bir veri deposu (data store) ile senkron tutuyorsanız ve useEffect’i yerleşimi ölçmek yerine başka sebeplerle kullanıyorsanız, bunun yerine sunucu taraflı render’ı destekleyen useSyncExternalStore’u kullanmayı düşünün.