İçeriğe geç

React Render Modeli

Orta 10 dk Çok sık karşılaşılır

Önce şunu oku: JavaScript Olay Döngüsü — setTimeout(0) Neden Sonra Yazıyor?

30 saniyede özet

React'in bir bileşeni yeniden çizmesi (render), ekranın değişmesi demek değil. React.memo ise ancak verdiğin değerler her seferinde aynı nesneyse işe yarar; yoksa hiçbir şey kazandırmaz.

Bileşenlerin durmadan yeniden çiziliyor gibi görünüyor. Endişelenmeden önce bilmen gereken bir şey var: yeniden çizmek ekranı değiştirmek demek değil.

  1. Bayt: Sayaca bir kez tıkladım, sayfadaki bütün bileşenler yeniden render oldu!

  2. Sen: Her yere memo koyalım o zaman.

  3. Bayt: Koydum. Hâlâ hepsi render oluyor, üstelik biraz daha yavaş.

  4. Bayt: Listeyi baştan yazmak mı pahalı, markete gitmek mi?

Render listeyi yazmak, commit markete gitmektir.
Adım adım oku
  1. State değişince React o bileşeni ve altındaki bütün çocukları yeniden çalıştırır.
  2. Yeni ağacı öncekiyle karşılaştırır ve gerçekten değişen tek düğümü bulur.
  3. Commit fazında DOM'a yalnızca o değişiklik yazılır; ekranın geri kalanı olduğu gibi kalır.
  4. React.memo ile sarılan ve propları aynı kalan bir çocuk, yeniden render edilmeden atlanır.

Listeyi yazmak, markete gitmek

Bir state değiştiğinde React iki ayrı iş yapar:

  1. Render fazı — bileşen fonksiyonlarını yeniden çalıştırır ve yeni bir element ağacı üretir. Bu faz saf olmalıdır; DOM’a hiçbir şey yazılmaz. React bu fazı iptal edip baştan başlatabilir — StrictMode’un geliştirmede bileşenleri iki kez çağırmasının sebebi budur.
  2. Commit fazı — yeni ağacı öncekiyle karşılaştırır ve yalnızca gerçekten değişen düğümleri DOM’a yazar.

Yani bir bileşenin “yeniden render olması”, ekranda bir şeyin değiştiği anlamına gelmez. Genellikle hiçbir şey değişmez.

Kafam karıştı, daha basit anlat

Render, alışveriş listesini yeniden yazmaktır. Ekranı güncellemek ise markete gitmektir. React listeyi sık yazar, ama markete yalnızca listede gerçekten bir şey değiştiyse gider.

Kendin gör

Varsayılan ayarda hiç optimizasyon yok. setCount çağır düğmesine bas, sonra adımla ve iki sayıyı karşılaştır: render sayısı ve DOM güncellemesi.

React render modeli — ne yeniden render olur, ne DOM’a yazılır

Tohum 3
boştaBaşlamak için “setCount çağır” düğmesine bas.
  • Appziyaret edilmedihenüz render edilmedi
  • Headerziyaret edilmedihenüz render edilmedi
  • SearchBoxziyaret edilmedihenüz render edilmedi
  • Listziyaret edilmedihenüz render edilmedi
  • Counterziyaret edilmedihenüz render edilmedi
  • Row 1ziyaret edilmedihenüz render edilmedi
  • Row 2ziyaret edilmedihenüz render edilmedi
  • Row 3ziyaret edilmedihenüz render edilmedi
  • Row 4ziyaret edilmedihenüz render edilmedi
Render sayısı
0
DOM güncellemesi
0
Boşa render
0
Hız
Adım 0

Şu an ne oldu?

Bekliyor — setCount çağır

React hiçbir şey yapmaz, ta ki bir state değişene kadar. "setCount çağır" düğmesine basıp render fazını adım adım izle.

Aklında kalsın: Render fazı ile commit fazı ayrı şeylerdir. Bunu ayırt etmek, "neden bu kadar çok render oluyor" sorusunun cevabının ilk yarısıdır.

Olay günlüğü (0)

Henüz olay yok. Oynat veya adımla.

Sırayla dene:

  • Hiçbir şey açık değilken. Tüm ağaç render oluyor, DOM’a yalnızca bir güncelleme gidiyor. Boşa render sayacı gerisini anlatıyor.
  • Sadece React.memo’yu aç. Header atlanıyor (prop’u sabit bir string), ama SearchBox ve List hâlâ render oluyor. Sarı uyarıyı oku — memoProp'ları değişmediyse bileşenin yeniden render edilmesini atlayan sarmalayıcı. Şartı referans eşitliğidir; inline prop geçiyorsan hiçbir şey yapmaz.Sözlükte gör → çalıştı, karşılaştırma başarısız oldu.
  • useCallback’i de aç. SearchBox artık atlanıyor. List hâlâ render oluyor.
  • useMemo’yu da aç. List ve bütün satırlar atlanıyor. Boşa render sıfır.
  • Satır sayısını 10 yap ve memo’yu kapat. Tek bir sayaç değişikliği on üç bileşeni render ediyor.
Hızlı kontrolBaşlangıç

Bir bileşenin "render olması" tam olarak ne demek?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Ebeveyn çizilince çocuklar da çizilir

App içindeki sayaç arttı. Altındaki List bileşeninin propları hiç değişmedi. List yeniden render edilir mi? Cevabı göster

Evet. Bir bileşen render edilince altındaki bütün bileşenler de render edilir; React propları kendiliğinden karşılaştırmaz.

Bu, propları değişmese bile geçerlidir. React propları varsayılan olarak karşılaştırmaz — karşılaştırmanın kendisi de bir maliyettir ve çoğu bileşen için render etmek daha ucuzdur.

App.tsx — sorunlu hâli
function App() {
const [count, setCount] = useState(0);
// Her render'da YENİ bir dizi ve YENİ bir fonksiyon
const items = products.filter((p) => p.active);
const onSearch = (q: string) => setQuery(q);
return (
<>
<Counter count={count} /> {/* prop gerçekten değişti — doğru render */}
<SearchBox onSearch={onSearch} />{/* prop "değişti" — çünkü yeni fonksiyon */}
<List items={items} /> {/* prop "değişti" — çünkü yeni dizi */}
</>
);
}
Kafam karıştı, daha basit anlat

Ebeveyn yeniden çizilince çocukları da yeniden çizilir, propları değişmemiş olsa bile. Çoğu zaman bu ucuzdur ve dert edilmez.

Hızlı kontrolBaşlangıç

`useState`'in set fonksiyonunu aynı değerle çağırırsan ne olur?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

React.memo’nun tek şartı

React.memo propları yüzeysel (shallow) karşılaştırır, yani referanslarına bakar. {} !== {} ve [] !== [] olduğu için, her render’da yeniden oluşturulan her nesne memo’yu boşa çıkarır.

Doğru kullanım üçünü birlikte gerektirir:

App.tsx — çalışan hâli
const List = React.memo(function List({ items }: Props) { /* ... */ });
function App() {
const items = useMemo(() => products.filter((p) => p.active), [products]);
// Not: setQuery'nin kendisi zaten kararlıdır; sarmalama gereken bu satır değil,
// her render'da yeniden oluşan inline fonksiyondur.
const onSearch = useCallback((q: string) => setQuery(q), []);
return (
<>
<SearchBox onSearch={onSearch} />
<List items={items} />
</>
);
}
Kafam karıştı, daha basit anlat

React.memo “eline aynı kutu mu geldi” diye bakar, kutunun içine bakmaz. Her seferinde yeni bir kutu verirsen, içi aynı olsa bile farklı sanır.

Hızlı kontrolOrta

Butona basıldığında `Child` yeniden render olur mu?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.
MemoTuzagi.tsx
1const Child = React.memo(({ onAction }: Props) => {
2 console.log("child render");
3 return <button onClick={onAction}>tikla</button>;
4});
5
6function Parent() {
7 const [count, setCount] = useState(0);
8
9 return (
10 <>
11 <button onClick={() => setCount(count + 1)}>{count}</button>
12 <Child onAction={() => console.log("aksiyon")} />
13 </>
14 );
15}
TypeScript JSXUTF-8LF

Her durumu, `React.memo`'nun işe yarayıp yaramadığına göre ayır.

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Sınıflandırılmamış

memo işe yarar

Prop referansları render'lar arası sabit

    İşe yaramaz

    Referans her render'da değişiyor

      Ama her yere memo koymak yanlış. Bu dersin en önemli cümlesi bu olabilir: boşa render her zaman bir sorun değildir.

      Render ucuzsa — birkaç div, biraz metin — yüz tane boşa render toplamda bir milisaniye bile etmez. useMemo ve useCallback da bedava değildir: bağımlılık dizisi karşılaştırması, ek bellek, ve okunması zorlaşan kod.

      Sıralama şu olmalı:

      1. Ölç. React DevTools Profiler hangi bileşenin gerçekten pahalı olduğunu söyler.
      2. Yapıyı düzelt. Çoğu zaman state’i aşağı taşımak veya children prop’u kullanmak memo’dan daha iyi çalışır.
      3. Sonra memo. Ve mutlaka üçlü olarak: memo + useMemo/useCallback.
      Yapısal çözüm — memo'suz
      // State'i yalnızca ihtiyaç duyan bileşene taşı:
      // artık setCount çağrısı App'i değil, sadece Counter'ı render eder
      function App() {
      return (
      <>
      <Counter /> {/* count state'i buranın içinde */}
      <ExpensiveList /> {/* App render olmadığı için hiç etkilenmiyor */}
      </>
      );
      }

      Aynı fikri bir banka uygulamasında görelim. Hesap ekranında her saniye tıklayan bir oturum sayacı var; bakalım hareket listesi bu tıklardan nasıl etkileniyor.

      Derinleş · Hesap hareketleri: sayaç tıklarken liste yerinde durur 4 dosya · ~115 satır · ilk okumada atlayabilirsin
      Proje dosyaları

      src/features/account/ TransactionList.tsx Memo ile sarılı hareket listesi; tutarlar kuruş cinsinden tam sayı.

      src/features/account/TransactionList.tsx
      import { memo } from 'react';
      export interface Transaction {
      readonly id: string; // core banking transaction id
      readonly description: string;
      readonly amountKurus: number; // integer, negative = money out
      readonly channel: 'FAST' | 'EFT' | 'CARD';
      }
      // Integer maths only: 123456 becomes "1.234,56 TL".
      export function formatTry(kurus: number): string {
      const abs = Math.abs(kurus);
      const lira = (abs - (abs % 100)) / 100;
      const sign = kurus < 0 ? '-' : '';
      return `${sign}${lira.toLocaleString('tr-TR')},${String(abs % 100).padStart(2, '0')} TL`;
      }
      interface Props {
      transactions: readonly Transaction[];
      onDispute: (transactionId: string) => void;
      }
      export const TransactionList = memo(function TransactionList({ transactions, onDispute }: Props) {
      return (
      <ul>
      {transactions.map((t) => (
      <li key={t.id}>
      {t.description} {formatTry(t.amountKurus)}
      {t.channel === 'CARD' && (
      <button type="button" onClick={() => onDispute(t.id)}>İtiraz et</button>
      )}
      </li>
      ))}
      </ul>
      );
      });

      src/features/account/ SessionCountdown.tsx Oturum sayacı kendi state'ini tutar, her tık yalnızca onu render eder.

      src/features/account/SessionCountdown.tsx
      import { useEffect, useState } from 'react';
      // The ticking state lives here, so a tick re-renders this component only.
      export function SessionCountdown({ expiresAt }: { expiresAt: number }) {
      const [now, setNow] = useState(() => Date.now());
      useEffect(() => {
      const id = setInterval(() => setNow(Date.now()), 1000);
      return () => clearInterval(id);
      }, []);
      const secondsLeft = Math.max(0, Math.ceil((expiresAt - now) / 1000));
      return <p role="timer">Oturum {secondsLeft} sn sonra kapanacak</p>;
      }

      src/features/account/ AccountScreen.tsx Liste useMemo, itiraz fonksiyonu useCallback ile sabit; bakiyeyi gizlemek listeyi render etmez.

      src/features/account/AccountScreen.tsx
      import { useCallback, useMemo, useState } from 'react';
      import { SessionCountdown } from './SessionCountdown';
      import { TransactionList, formatTry, type Transaction } from './TransactionList';
      interface Props {
      iban: string;
      balanceKurus: number;
      transactions: readonly Transaction[];
      sessionExpiresAt: number;
      openDispute: (iban: string, transactionId: string) => void; // stable, from the parent
      }
      export function AccountScreen({ iban, balanceKurus, transactions, sessionExpiresAt, openDispute }: Props) {
      const [onlyOutgoing, setOnlyOutgoing] = useState(false);
      const [balanceHidden, setBalanceHidden] = useState(false);
      // Same references until their inputs really change, so memo can skip the list.
      const visible = useMemo(
      () => transactions.filter((t) => !onlyOutgoing || t.amountKurus < 0),
      [transactions, onlyOutgoing],
      );
      const onDispute = useCallback((id: string) => openDispute(iban, id), [iban, openDispute]);
      return (
      <section>
      <p>{balanceHidden ? '•••••' : formatTry(balanceKurus)}</p>
      <button type="button" onClick={() => setBalanceHidden((h) => !h)}>Bakiyeyi gizle/göster</button>
      <button type="button" onClick={() => setOnlyOutgoing((o) => !o)}>Yalnızca giden</button>
      <SessionCountdown expiresAt={sessionExpiresAt} />
      <TransactionList transactions={visible} onDispute={onDispute} />
      </section>
      );
      }

      src/features/account/ AccountScreen.alternative.tsx Şöyle de yazılabilirdi: sayaç en üstte, dizi ve fonksiyon her seferinde yeni. Bak, liste her saniye baştan render oluyor.

      src/features/account/AccountScreen.alternative.tsx
      import { useEffect, useState } from 'react';
      import { TransactionList, type Transaction } from './TransactionList';
      interface Props {
      iban: string;
      transactions: readonly Transaction[];
      sessionExpiresAt: number;
      openDispute: (iban: string, transactionId: string) => void;
      }
      // Another way to write it. The list is still wrapped in memo, yet it re-renders every second.
      export function AccountScreen({ iban, transactions, sessionExpiresAt, openDispute }: Props) {
      const [onlyOutgoing, setOnlyOutgoing] = useState(false);
      const [now, setNow] = useState(() => Date.now());
      useEffect(() => {
      const id = setInterval(() => setNow(Date.now()), 1000); // the whole screen ticks
      return () => clearInterval(id);
      }, []);
      // A new array and a new function on every tick: memo's comparison never matches.
      const visible = transactions.filter((t) => !onlyOutgoing || t.amountKurus < 0);
      const onDispute = (id: string) => openDispute(iban, id);
      return (
      <section>
      <p role="timer">{Math.max(0, Math.ceil((sessionExpiresAt - now) / 1000))} sn</p>
      <button type="button" onClick={() => setOnlyOutgoing((o) => !o)}>Yalnızca giden</button>
      <TransactionList transactions={visible} onDispute={onDispute} />
      </section>
      );
      }
      Hızlı kontrolİleri

      State'i yukarı taşımak (lifting state up) ne zaman performans sorununa dönüşür?

      Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

      Tuzaklar: anahtar (key)

      Liste elemanlarında key olarak dizinin indeksini kullanmak, React’in yanlış elemanı yeniden kullanmasına yol açar. Başa bir eleman eklendiğinde bütün indeksler kayar; React “1 numaralı eleman değişti” sanır ve DOM durumunu (input içeriği, odak, animasyon) yanlış satıra taşır.

      Liste anahtarı
      {items.map((item, i) => (
      <Row key={i} item={item} /> // başa ekleme yapılınca her şey kayar
      ))}
      {items.map((item) => (
      <Row key={item.id} item={item} /> // kimlik elemanın kendisinden gelir
      ))}

      Kendini sına

      Şimşek turu1/5

      Bir bileşenin yeniden render olması, ekranda bir şeyin değiştiği anlamına gelir.

      Soru 1/3Orta

      Bir bileşen yeniden render oldu. Bu ne anlama gelir?

      Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

      Aklında kalacak üç şey

      1. 1 İki ayrı adım var: render'da bileşen fonksiyonu yeniden çalışır, commit'te yalnızca gerçekten değişen kısım ekrana yazılır. Render ekran güncellemesi demek değildir.
      2. 2 Varsayılan davranış: ebeveyn render olunca bütün çocuklar da render olur, propları değişmese bile. Bundan çıkış yolu React.memo'dur.
      3. 3 memo referans karşılaştırması yapar. Her render'da yeniden oluşan bir dizi ya da fonksiyon memo'yu etkisiz bırakır; useMemo ve useCallback olmadan memo eklemek zarardır.
      Sonraki kapı useEffect'in içindeki sayaç neden hep ilk değerde takılı kalıyor? useEffect — Bağımlılıklar ve Temizlik · 10 dk

      5 kart sonraki derste seni bekliyor

      0/5 kart bu dersten toplandı

      Bu dersin üstüne kurulanlar

      Bunlar bu dersi temel alıyor; hazır olduğunda devam edebilirsin.