React'te Render, Effects ve Refs Nasıl Çalışır?

Bir daha asla kafanız karışmasın ve daha iyi React kodu yazın.

S

Sedat Demir

hepsedat@gmail.com

4 gün önce okundu

27 Ocak, 2025

React'te Render, Effects ve Refs Nasıl Çalışır?

Giriş

React'in tüm render döngüsünü ve tarayıcıyla nasıl çalıştığını anlamak biraz zor gelebilir.

Modern React dokümantasyonunu okusanız bile, tüm görseller göz önüne alındığında kafa karıştırıcı olabiliyor.

Basit ve doğrudan olarak ilerleyelim. Bu, tam akışı anlamama yardımcı olmak için kendim için yazdığım bir blog.

Düşünceleri harekete geçirmek için şu kod parçasıyla başlayalım:

function ExploringReactRefs() {
  // Bu ref neden null ile başlıyor?
  // Gerçek değerini ne zaman alır?
  const divRef = useRef<HTMLDivElement>(null);

  // Bu çalışıyormuş gibi hissettiriyor... ama gerçekten öyle mi?
  // Bu efekt tam olarak ne zaman çalışır?
  useEffect(() => {
    console.log("Effect:", divRef.current?.getBoundingClientRect());
  }, []);

  // Bu efekt neden farklı?
  // Neden useEffect yerine buna ihtiyaç duyabiliriz?
  useLayoutEffect(() => {
    console.log("Layout Effect:", divRef.current?.getBoundingClientRect());
  }, []);

  // Bu callback ref yaklaşımının özelliği nedir?
  // Bu fonksiyon tam olarak ne zaman çağrılır?
  // Aşağıdaki ikinci div'de handleRef'in kullanıldığı yere bakın.
  const handleRef = (node: HTMLDivElement | null) => {
    if (node) {
      console.log("Callback ref:", node.getBoundingClientRect());
    }
  };

  return (
    <div className="flex gap-4">
      {/* Bu elemente divRef üzerinden ne zaman erişebiliriz? */}
      <div ref={divRef}>Using useRef</div>

      {/* Bu useRef'ten nasıl farklı? */}
      <div ref={handleRef}>Using callback ref</div>
    </div>
  );
}

State güncellemesi ve render'lar

Bir bileşenin state'i güncellendiğinde, React bileşeni yeniden render eder. Bir bileşenin yeniden render edilmesi, tüm alt bileşenlerini de yeniden render eder (evet, bunu optimize edebilirsiniz, ancak buradaki konu bu değil).

Ve! Efektler yalnızca bağımlılıkları değiştiğinde çalışır. Eğer bağımlılık dizisi boşsa, yalnızca bileşen oluşturulduğunda (mount edildiğinde) bir kez çalışır.

Bu konuyu daha iyi anlamak için bir kod parçasına göz atalım:

function Component() {
  // 1. Bağımlılık dizisi yok - HER render'da çalışır
  useEffect(() => {
    // Effect çalışır
    return () => {
      /* Bir sonraki efektten önce cleanup çalışır */
    };
  }); // Bağımlılık dizisi eksik

  // 2. Boş dizi - yalnızca mount/unmount'da çalışır
  useEffect(() => {
    // Effect bir kez çalışır
    return () => {
      /* Unmount'da cleanup çalışır */
    };
  }, []);

  // 3. Bağımlılıklarla - bağımlılıklar değiştiğinde çalışır
  useEffect(() => {
    // Effect, count değiştiğinde çalışır
    return () => {
      /* Count değiştiğinde bir sonraki efektten önce cleanup çalışır */
    };
  }, [count]);

  // Aynı kurallar useLayoutEffect için de geçerlidir
}

Mount, bileşenin oluşturulduğu anlamına gelir.

Unmount ise bileşenin yok edildiği veya daha basit bir ifadeyle DOM'dan kaldırıldığı anlamına gelir. Gençliğimde bunun sayfadan uzaklaşmak anlamına geldiğini düşünürdüm. Ancak bu, bir bileşeni koşullu olarak render ediyorsanız da geçerli olabilir.

Kod parçasına geri dönelim

Bu kod parçasında birkaç şey oluyor.

Bir bileşen render edildiğinde, iki ana aşamadan geçer:

  1. Render aşaması.

  2. Commit aşaması.

Bunları daha basit terimlere ayıracağız.

Şimdilik, her render gerçekleştiğinde bu iki aşamanın yürütüldüğünü anlayın: Render ve Commit.

Sanal DOM

Render aşamasına geçmeden önce, Sanal DOM hakkında konuşalım.

Konuyu anlamayan pek çok kişi hemen “Sanal DOM React'i daha hızlı hale getirmek içindir” demeye başlıyor. Bu biraz komik çünkü durum tam olarak öyle değil. Bugün Solid.js gibi Sanal DOM'a sahip olmayan ve React'ten daha hızlı olan UI kütüphaneleri var. Bu yüzden bu ifade oldukça kafa karıştırıcı ve yanlış.

Ben, işlerin yüksek seviyede nasıl çalıştığını açıklayacağım.

Gerçekte React, basit bir Sanal DOM yerine Fiber mimarisini kullanır. Bu, React'in işi parçalara ayırmasına ve önceliklendirmesine izin verir. Bu, temelleri anlamamız için faydalıdır.

Sanal DOM nedir?
Sanal DOM, yalnızca JavaScript nesneleridir. Gerçek DOM'un bir temsilidir.

// Sanal DOM sadece JavaScript nesneleridir
const virtualElement = {
  type: "div",
  props: { className: "container" },
  children: [
    {
      type: "span",
      props: { children: "Hello" },
    },
  ],
};

// Gerçek DOM, tarayıcının gerçek API'leridir
const realElement = document.createElement("div");
realElement.className = "container";

İşte burada ilk “maliyeti” fark ettik bile. DOM'un bir temsilini bellekte depoluyoruz. Şimdi bu büyük bir sorun değil. Milyonlarca web sitesi React kullanıyor. Önemli olan, bir şeyleri anlamadan konuşmak yerine, olaylara ilk prensiplerden bakmak ve neler olduğunu gerçekten anlamaktır.

Peki... React bunu neden kullanıyor?
Burada bahsetmek istediğim iki temel felsefe var.

  1. Farklı platformlar

    Sanal bir DOM'a sahip olan React, tarayıcının DOM'una bağlı değildir.

    Bu, React'in farklı platformlara render edebileceği anlamına gelir.

    React Native'in var olması ve çalışmasının nedeni de budur. Mobil uygulamalar tarayıcı DOM'unu kullanmıyor 😄

    İşte bunu anlamamız için bir pseudo kod:

    // React farklı hedeflere render edebilir
    function render(virtualElement) {
      switch (environment) {
        case "web":
          return renderToDOM(virtualElement);
        case "mobile":
          return renderToNative(virtualElement);
        case "server":
          return renderToString(virtualElement);
      }
    }
  2. Toplu güncellemeler
    Daha önce tartıştığımız gibi, React bir state güncellemesi olduğunda tüm bileşeni (alt bileşenleri dahil) yeniden render eder.

    Bu, state güncellemeleri olduğunda, birçok DOM değişikliğiyle sonuçlanabileceği anlamına gelir.

    Sanal DOM ile React bu güncellemeleri toplu olarak yapabilir. Yapılması gereken tüm değişiklikleri belirleyebilir ve commit aşaması yürütüldüğünde bunları tek bir seferde uygulayabilir.

    // Sanal DOM olmadan
    state.change1(); // DOM güncellemesi
    state.change2(); // DOM güncellemesi
    state.change3(); // DOM güncellemesi
    
    // Sanal DOM ile
    state.change1(); // Sanal ağacı güncelle
    state.change2(); // Sanal ağacı güncelle
    state.change3(); // Sanal ağacı güncelle
    // İşlem sonunda tek bir DOM güncellemesi!

Render aşaması

Son olarak render fazından bahsedelim.

Bu, render döngüsünün ilk aşamasıdır.

Öğrenirken bazen beni rahatsız eden şeylerden biri, insanların kullanmaya çalıştığı tüm terminolojilerdir.

Buna "state değişikliğinden DOM değişikliğine giden ilk adım" da diyebiliriz.

Biraz pseudo koda bakalım:

// RENDER AŞAMASI
function renderPhase(newState) {
  // 1. React, bileşenleri çağırarak Sanal DOM'u oluşturur/günceller
  function handleStateUpdate() {
    // Yeni Sanal DOM ağacı oluştur
    const newVirtualDOM = {
      type: "div",
      props: { className: "app" },
      children: [
        {
          type: "span",
          props: { children: newState },
        },
      ],
    };

    // 2. Reconciliation (Karşılaştırma)
    // React yeni Sanal DOM'u öncekiyle karşılaştırır
    // Gerçek DOM'da neyin değişmesi gerektiğini belirler
    const changes = diff(previousVirtualDOM, newVirtualDOM);
    // Gerekli DOM operasyonlarının bir listesini oluşturur
    // [{type: 'UPDATE', path: 'span/textContent', value: newState}]
  }
}

Yeni state ile React yeni bir Sanal DOM ağacı oluşturur.

React, bu yeni Sanal DOM ağacını kullanarak gerçek DOM'da ne gibi değişiklikler yapılması gerektiğini belirler.

Bunu, yeni Sanal DOM ağacını öncekiyle karşılaştırarak yapar.

Artık React, tam olarak hangi değişikliklerin yapılması gerektiğini bilir ve her state güncellemesinde tüm DOM'u güncellememize gerek yoktur.

Şimdi, tüm DOM'u güncellemenin çok maliyetli olacağını düşünebilirsiniz. Gerçek cevap şu ki "olabilir". Ne inşa ettiğinize bağlıdır. Aynı zamanda iyi de olabilir. Bu yüzden kesin olarak ÇOK maliyetli olacağını söyleyemeyiz (bu sadece temel ilkelerden yola çıkarak, dikkatli ve derinlemesine düşündüğüm bir şey).

Commit aşaması

Tamam. Artık hangi değişiklikleri yapmamız gerektiğini biliyoruz.

Commit aşaması genellikle "React DOM'u günceller" şeklinde özetlenir. Ancak bu biraz daha derindir.

Not: Eğer event loop hakkında bilginiz yoksa, devam etmeden önce bunu okumanızı öneririm. Ücretsiz bir ileri düzey JS kitabı var. Event loop hakkında daha fazla bilgi edinmek istiyorsanız 12, 13 ve 14. bölümlere bakabilirsiniz. Ayrıca MDN ve Youtube'dan da inceleme yapabilirsiniz.

Biraz pseudo koda bakalım:

// 1. React'in Commit Aşaması (Senkron JavaScript)
// Bu ana thread'de çalışır
function commitToDOM() {
 // React DOM API'lerini çağırır
 // Her çağrı call stack'e eklenir
 mutateDOM() {
   document.createElement()
   element.setAttribute()
   element.appendChild()
   // ...
 }

 // useLayoutEffect’i hatırlıyor musunuz?
 // Şimdi tüm layout efektlerini çalıştıracağız
 // Bu işlem senkron şekilde gerçekleşir
 // Buradaki kod da call stack'e eklenir
 runLayoutEffects()

 // useEffect'i daha sonra sıraya al
 queueMicrotask(() => {
   runEffects()
 })
}

// commitToDOM() tamamlandı - tarayıcının çalışma zamanı

// Layout Hesaplama
// Paint
// Composite


// 3. Microtask Sırası
// Şimdi useEffect çalışır

Tarayıcının nasıl çalıştığı bu yazının kapsamı dışında kalıyor. Ancak bu konu oldukça ilgi çekici. 2025'te derinlemesine keşfetmek istediğim şeylerin listesinde yer alıyor, lmao. Hidden class'lar ve benzeri şeyler üzerine biraz araştırma yaptım. Hızlıca bu noktalara göz atalım, sonra konuya geri dönelim:

  1. Layout'u hesaplama: Tarayıcı tam konumları ve boyutları hesaplar.

  2. Paint: Tarayıcı layout sonuçlarını görsel piksellere dönüştürür.

  3. Composite: Tarayıcı katmanları birleştirerek son ekran görüntüsünü oluşturur.

İlk şey: Yapmamız gereken güncellemeleri artık bildiğimiz için, senkron JavaScript kodunu çalıştırıyoruz (mutateDOM()).

Event loop’u anlamak için küçük bir kod örneğine göz atalım:

function mutateDOM() {
  document.createElement();
  element.setAttribute();
  element.appendChild();
}

mutateDOM();

Her çağrı call stack’e eklenir. Daha sonra tarayıcı bunu yukarıdan aşağıya doğru temizler:

1. element.appendChild()
2. element.setAttribute()
3. document.createElement()
4. mutateDOM()

Bir stack, LIFO (Last In First Out) yani "Son Giren İlk Çıkar" veri yapısına sahiptir.

Layout efektlerini çalıştırdığımızda, senkron JavaScript kodunu çalıştırıyoruz. Fonksiyon çağrısı ve içerdikleri call stack'e eklenir. Eğer yakından takip ettiyseniz, layout efektlerinin bağımlılıkları değiştiğinde bu kodun yeniden çalıştığını fark etmişsinizdir. Bu, tarayıcının kendi işini yapmadan önce daha fazla senkron kodun geçmesi gerektiği anlamına gelir. (React'in useLayoutEffect kullanırken dikkatli olunmasını önermesinin nedeni budur).

Her şeyi detaylı bir şekilde anlatmaya ve pratik noktalarla ilişkilendirmeye çalışıyorum, böylece bunu gerçekten anlayabiliriz.

Daha sonra normal efektleri çalıştırırız. Bunlar örneğimizde queueMicrotask() ile sıraya alınır. ANCAK, gerçekte React kendi planlama sistemini kullanır. Ancak temelleri anlamak için bunu bir microtask sırası olarak görmenin yardımcı olacağını düşündüm.

Tarayıcı kendi işini yaparken, önce tüm call stack'leri temizler, ardından microtask sırasını çalıştırır.

Ref'ler

Şimdi, React 19 çıktı.

Bunun detaylarına girmeyi planlamıyorum, bunu gelecek bir blog yazısında yapacağım.

Evet. Orijinal kod parçasındaki ref'lere odaklanalım.

const divRef = useRef<HTMLDivElement>(null);

Bu ref, render aşamasında oluşturulur. İlk render sırasında DOM elementi henüz olmadığı için null ile başlar. React, değişiklikleri DOM'a commit ettikten sonra gerçek değerini alır. Ancak yalnızca useRef kullanarak bunun tam olarak ne zaman gerçekleştiğini bilemeyiz.

Bu yüzden ref'i kullanmadan önce her zaman null olup olmadığını kontrol etmeniz gerekir.

if (divRef.current) {
  console.log(divRef.current.getBoundingClientRect());
}

Callback ref kullandığınızda ne olur?

const handleRef = (node: HTMLDivElement | null) => {
  if (node) {
    console.log("Callback ref:", node.getBoundingClientRect());
  }
};

Callback ref, öğe DOM’a eklendiğinde hemen çağrılır. Callback ref’in doğru zamanda çalışacağından %100 emin olabilirsiniz. Öğe DOM’dan kaldırıldığında bu ref null olur, böylece gerekli temizleme işlemlerini yapabilirsiniz. Ayrıca useLayoutEffect’ten önce çalışır. Bu yöntem, anında DOM ölçümleri veya kurulum işlemleri için en iyi yoldur.

Bir elementin boyutlarını almam gerekiyor... bunu yapmak için en iyi zaman ne zaman?
Diyelim ki bir tooltip'in konumunu bilmeniz gerekiyor:

function Tooltip({ text, targetRef }) {
  const tooltipRef = useRef(null);

  // Yanlış: Flicker’a neden olabilir
  // Neden?
  // Çünkü bu işlem DOM boyandıktan sonra gerçekleşir
  // Tooltip önceki konumunda görünebilir
  // Sonra bu kod çalıştığında konumu değişir
  useEffect(() => {
    const targetRect = targetRef.current.getBoundingClientRect();
    tooltipRef.current.style.top = `${targetRect.bottom}px`;
  }, []);

  // Daha iyi: Flicker yok
  // Neden?
  // Çünkü bu işlem DOM boyanmadan önce gerçekleşir
  // Tooltip direkt son konumunda görünür
  useLayoutEffect(() => {
    const targetRect = targetRef.current.getBoundingClientRect();
    tooltipRef.current.style.top = `${targetRect.bottom}px`;
  }, []);

  // En iyi: En doğrudan yöntem
  // Neden?
  // Çünkü bu işlem DOM eklenir eklenmez gerçekleşir
  // (layout efekt DOM eklemesinden sonra çalışır)
  const handleRef = (node) => {
    if (node) {
      const targetRect = targetRef.current.getBoundingClientRect();
      node.style.top = `${targetRect.bottom}px`;
    }
  };

  return <div ref={handleRef}>{text}</div>;
}

Cleanup fonksiyonları ne zaman çalışır?
Bir render'dan sonra, React efektleri çalıştırmadan HEMEN ÖNCE (yalnızca bağımlılıklar değiştiyse), önceki değerlerle cleanup fonksiyonlarını çalıştırır. Ardından yeni efektleri yeni değerlerle çalıştırır. Veya tabii ki bileşen unmount olursa.