Mert Özen Her Satırda Daha İleri
Frontend React — #16

React 18'den 19.3'e: Concurrent Rendering, Actions, use(), ref as prop, Activity, useEffectEvent, ViewTransition ve Fragment Ref'leri

Mert Özen 15 Kas 2026 12 dk 4 görüntülenme
React 18'den 19.3'e: Concurrent Rendering, Actions, use(), ref as prop, Activity, useEffectEvent, ViewTransition ve Fragment Ref'leri

React 18 concurrent rendering'in temelini (createRoot, batching, transition'lar), 19 Actions, use() ve ref'in props olmasını, 19.2 Activity ve useEffectEvent'i, 19.3 ise stable ViewTransition ve Fragment ref'lerini getirdi. Bookstore'a ViewTransition ve Fragment ref ekliyor, React Router 8.5'in deprecation'ını uyguluyor ve küçük bir React 18 projesini 18.3 uyarıları, TypeScript codemod'u ve elle düzeltmelerle 19.3'e yükseltiyoruz.

Sürüm notu: Bu yazıdaki örnekler React 18.3 ve 19.3, @types/react 18.3 ve 19.3, types-react-codemod 3.5, React Router 8.5, TypeScript 7.0, Vite 8.3, Vitest 5.1 ve Node.js 24 ile denenmiştir; tarayıcı davranışı Playwright 1.65 ile headless Chromium'da gözlenmiştir. npx codemod@latest react/19/migration-recipe çalıştırılmadı.

Önce Tahmin Et

Serinin son yazısı. Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) React istemcisini baştan React 19.3 ile yazdık; ama katılacağın bir React projesi büyük ihtimalle 18 ile başlamış, bir kısmı hâlâ eski API'lerde. Bu yazıda 18'den 19.3'e gelen değişiklikleri seride kullandığımız yerlerle eşleştiriyor, 19.3'ün iki stable yeniliğini Bookstore'a ekliyor ve küçük bir React 18 projesini gerçekten yükseltiyoruz.

Üç soru. React 18.3 ile sorunsuz çalışan bir component'te ReactDOM.render, string ref ve function component'te defaultProps var; React 19.3'e geçince ilk hata TypeScript'ten mi gelir, çalışma sırasında mı? TypeScript codemod'u beş tip hatasından kaçını düzeltir? React Router 8.5 kendi viewTransition API'lerini deprecated ilan etti; yerine ne yazıyorsun?

16-release-timeline-tr.png

React 18: Concurrent Rendering'in Temeli

React 18'in (Mart 2022) asıl değişikliği görünmeyen kısımdaydı: render artık bölünebiliyor ve öncelik sırasına konabiliyor. Bunun kapısı createRoot; ikinci yazıdaki main.tsx'in ilk satırı. Eski ReactDOM.render 18'de çalışmaya devam etti ama uygulamayı React 17 davranışında tuttu. Seride bu temelin üzerine kurduğumuz her şey 18'den geliyor: dördüncü yazıdaki otomatik batching (event handler dışındaki, örneğin promise içindeki setState'lerin de tek render'da birleşmesi), on ikinci yazıdaki useTransition ve useDeferredValue, yedinci yazıdaki useId ve useSyncExternalStore, on beşinci yazıda Next.js'in kullandığı streaming SSR (server-side rendering; sayfanın sunucuda render edilip parça parça gönderilmesi). StrictMode'un geliştirmede effect'leri iki kez çalıştırması da 18 ile geldi.

React 19: Actions, use() ve ref'in Props Olması

React 19 (5 Aralık 2024) seride en çok kullandığımız sürüm. startTransition async fonksiyon kabul etmeye başladı ve bu fonksiyonlara Action denildi; useActionState, useFormStatus, useOptimistic ve <form action> onuncu yazının konusuydu. use() render içinde promise ve Context okumayı sağladı (sekizinci ve yedinci yazılar). ref sıradan bir props oldu, forwardRef gereksizleşti; Context'in kendisi provider olarak render edilebildi (<ThemeContext value>). Bir de document metadata desteği geldi: component içinde render edilen <title>, <meta> ve <link>'i React <head>'e taşıyor. Bunu bu yazıda kitap sayfasına ekledik:

      {/* React 19 document metadata: <title> <head>'e taşınır */}
      <title>{`${book.title} · Bookstore`}</title>

Angular'da aynı işi Title service'i ya da route'un title'ı yapıyor; React'te ayrı bir service yok, başlık onu kullanan component'in içinde duruyor.

19.1 ve 19.2: Owner Stack, Activity ve useEffectEvent

19.1 (Mart 2025) geliştiriciye dönük bir sürümdü: Owner Stack (bir hatada, sorunlu component'i hangi component'lerin render ettiğini gösteren, yalnız geliştirmede çalışan yığın) ve captureOwnerStack geldi; useId'nin ürettiği değerler geçerli CSS selector'ı olacak şekilde «r123» biçimine geçti. Snapshot testlerinde ya da CSS'te eski :r1: biçimine güvenen kod varsa burada kırılır.

19.2 (1 Ekim 2025) iki önemli hook ekledi. <Activity> ile on ikinci yazıda stok sayfasının sekmelerini state'lerini kaybetmeden gizledik. useEffectEvent ile altıncı yazıda effect'ten tetiklenen ama effect'in bağımlılığı olmaması gereken mantığı ayırdık. Server Components tarafında cacheSignal geldi; Chrome DevTools'ta React'e özel Performance Tracks de bu sürümde eklendi. React Compiler 1.0 da aynı ay (7 Ekim 2025) stable oldu.

19.3: ViewTransition ve React Router 8.5

React 19.3 (9 Eylül 2026) iki deneysel API'yi stable yaptı. İlki <ViewTransition>: tarayıcının View Transition API'sini kullanarak, bir transition içinde gerçekleşen güncellemeleri animasyonla gösteriyor. React Router 8.5 (19 Ekim 2026) bu yüzden kendi viewTransition props'unu ve useViewTransitionState'i deprecated ilan etti; v8 boyunca çalışmaya devam edecekler, v9'da kalkacaklar. Önerilen kullanım, kalıcı bir layout'ta <Outlet />'i sarmak:

      <main>
        {/* React 19.3: router gezinmeleri transition içinde çalışır, sayfa geçişi cross-fade olur (RR'nin viewTransition'ının yerine) */}
        <ViewTransition>
          <Outlet />
        </ViewTransition>
      </main>

Router gezinmeleri zaten transition içinde çalıştığı için başka bir şey gerekmiyor. Önce ve sonra Playwright ile ölçtük (tarayıcının document.startViewTransition'ını sayarak):

before: catalog → Dune: startViewTransition calls = 0, document.title = "Bookstore"
before: pager "Next": scrollY 1073 → 1073, top of the list -713px → -713px from the top of the viewport
after:  catalog → Dune: startViewTransition calls = 2, document.title = "Dune · Bookstore"
after:  pager "Next": scrollY 1073 → 360, top of the list -713px → 0px from the top of the viewport

Değişiklikten önce hiç view transition yoktu; sonra katalogdan kitap sayfasına geçiş iki tane başlattı. Loader'ı olmayan sepet sayfasına geçişte bir, loader'ı olan kitap sayfasına geçişte iki çağrı saydık; ikincisinde router önce "Loading page…" göstergesini, sonra yeni sayfayı gösteriyor ve iki güncelleme de transition içinde:

catalog → cart (no loader): 1
catalog → Dune (bookLoader): 2

Varsayılan animasyon cross-fade. Yöne göre farklı animasyon için addTransitionType ile güncellemeye bir tür ekleyip <ViewTransition enter={{ ... }}> ile ona göre class seçebiliyorsun. Transition olmayan, acil güncellemeler animasyonsuz kalıyor; inputa yazılan harf beklemiyor.

Fragment Ref'leri

19.3'ün ikinci stable yeniliği <Fragment ref>. Ref'i FragmentInstance veriyor: Fragment'ın çocukları üzerinde DOM'u (tarayıcının sayfayı tuttuğu node ağacı) değiştirmeden çalışan focus, scrollIntoView, addEventListener, observeUsing gibi method'lar. Katalogda sayfa butonları listenin altında; "Next"e basan kullanıcı yeni sayfanın ortasından başlıyordu. BookList ref almıyor, Fragment ref'i onu değiştirmeden listenin DOM'una ulaşıyor:

  const searchInput = useRef<HTMLInputElement>(null);
  // BookList ref props'u almıyor: Fragment ref'i, BookList'i değiştirmeden render ettiği DOM'a ulaşır
  const results = useRef<FragmentInstance>(null);
// ...
  function goToPage(next: number) {
    // Sayfa butonları listenin altında: sonuçların üstünü yeniden görünür yap
    results.current?.scrollIntoView(true);
    setSearchParams((params) => {
      params.set('page', String(next));
      return params;
    });
  }
// ...
            <Fragment ref={results}>
              <BookList books={data.items} quantityOf={quantityOf} onAdd={add} />
            </Fragment>

Yukarıdaki ölçümün ikinci satırı sonucu gösteriyor: listenin üstü -713 pikselden 0'a geldi. Dürüst olmak gerekirse burada listenin etrafındaki div'e ref vermek de aynı işi görürdü; Fragment ref'in asıl yeri wrapper'ı olmayan kardeş element'ler ve ref'i dışarı açmayan, değiştiremediğin library component'leri. İlk derlemede de bir sürpriz çıktı:

$ npx tsc -b
src/features/catalog/CatalogPage.tsx(34,22): error TS2339: Property 'scrollIntoView' does not exist on type 'FragmentInstance'.

FragmentInstance @types/react'te boş bir interface; DOM method'larını @types/react-dom ekliyor. Projede react-dom'un ana tip dosyası hiç yüklenmediği için (yalnız react-dom/client import ediliyordu) vite-env.d.ts'e bir referans ekledik:

/// <reference types="vite/client" />
// FragmentInstance DOM method'larını (focus, scrollIntoView, observeUsing…) react-dom'un tiplerinden alır
/// <reference types="react-dom" />

19.3'ün Diğer Yenilikleri

browser() (react-dom): server-side rendering yapan bir uygulamada, yalnız tarayıcıda anlamlı olan bir component'i (yerel saat dilimi, localStorage) use(browser()) ile sunucu render'ından çıkarıyor; sunucuda en yakın Suspense fallback'i render ediliyor. Bizim SPA'mızda (single-page application; tek HTML sayfasında çalışan uygulama) bir karşılığı yok, ama on beşinci yazıdaki gibi bir Next.js projesinde useEffect + mounted state'i hilesinin yerini alıyor. React Trusted Types'ı da destekliyor: require-trusted-types-for 'script' uygulayan bir sitede, policy'lerden geçmiş TrustedHTML değerlerini artık string'e çevirmeden DOM'a veriyor (Frontend Güvenliği serisindeki "Trusted Types'ı Production'a Almak: Angular Policy'leri, Üçüncü Parti Kütüphaneler ve Baseline 2026"). Server Component'ler de bir 'use client' module'ünden import ettikleri Context'i, ayrı bir provider component'i yazmadan doğrudan render edebiliyor. Changelog'da dikkat çekici iki satır daha var: birbirinden bağımsız transition'lar artık tek bir render'a bağlanmıyor ve StrictMode effect'leri hydration sırasında da iki kez çalıştırıyor. Changelog'da breaking change olarak işaretlenmiş bir madde yok.

Yükseltme Kontrol Listesi: Küçük Bir React 18 Projesini 19.3'e Taşımak

Snapshot'taki upgrade-lab/ klasöründe, eski API'leri bilerek kullanan küçük bir component var. Başlangıç hâli:

import { Component, createContext, forwardRef, useContext, useRef } from 'react';
import ReactDOM from 'react-dom';

const ThemeContext = createContext('light');

// Function component'te defaultProps
function PriceTag({ price, currency }: { price: number; currency?: string }) {
  const theme = useContext(ThemeContext);
  return <span className={theme}>{price} {currency}</span>;
}
PriceTag.defaultProps = { currency: 'TRY' };

// forwardRef yalnız ref'i <input>'a iletmek için
const SearchBox = forwardRef<HTMLInputElement, { placeholder: string }>((props, ref) => <input ref={ref} {...props} />);

// String ref kullanan bir class component
class BookTitle extends Component<{ title: string }> {
  componentDidMount() {
    (this.refs.heading as HTMLElement).dataset.ready = 'yes';
  }
  render() {
    return <h2 ref="heading">{this.props.title}</h2>;
  }
}

export function App() {
  const search = useRef<HTMLInputElement>(null);
  const renders = useRef<number>();
  renders.current = (renders.current ?? 0) + 1;
  let box: HTMLDivElement | null = null;
  return (
    <ThemeContext.Provider value="dark">
      <div ref={(node) => (box = node)} onClick={() => box?.focus()}>
        <BookTitle title="Dune" />
        <PriceTag price={489} />
        <SearchBox ref={search} placeholder="Search" />
      </div>
    </ThemeContext.Provider>
  );
}

export function mount(container: HTMLElement) {
  ReactDOM.render(<App />, container);
}

React 19 yükseltme rehberinin ilk adımı önce 18.3'e geçmek: 18.2 ile aynı ama 19'da kalkacak API'ler için uyarı veriyor. Uyarıları bir Vitest testinde topladık:

$ npx vitest run --silent=false        # react@18.3.1
stdout | src/legacy.test.tsx > renders the app and lists what React warns about
<div><h2 data-ready="yes">Dune</h2><span class="dark">489 TRY</span><input placeholder="Search"></div>
- Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot
- Warning: Component "BookTitle" contains the string ref "heading". Support for string refs will be removed in a future major release. We recommend using useRef() or createRef() instead. Learn more about using refs safely here: https://reactjs.org/link/strict-mode-string-ref
- Warning: PriceTag: Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.

Sonra React 19.3'e ve tiplerine geçtik (npm install --save-exact react@19.3.1 react-dom@19.3.1, @types/react@19.3.0, @types/react-dom@19.3.0). Önce Tahmin Et'teki ilk sorunun cevabı: ikisi birden. TypeScript beş hata verdi, test de çalışma sırasında düştü:

$ npx tsc --noEmit
src/legacy.tsx(19,11): error TS2339: Property 'refs' does not exist on type 'BookTitle'.
src/legacy.tsx(22,16): error TS2322: Type 'string' is not assignable to type 'Ref<HTMLHeadingElement> | undefined'.
src/legacy.tsx(28,19): error TS2554: Expected 1 arguments, but got 0.
src/legacy.tsx(33,12): error TS2322: Type '(node: HTMLDivElement | null) => HTMLDivElement | null' is not assignable to type 'Ref<HTMLDivElement> | undefined'.
  Type '(node: HTMLDivElement | null) => HTMLDivElement | null' is not assignable to type '(instance: HTMLDivElement | null) => void | (() => VoidOrUndefinedOnly)'.
    Type 'HTMLDivElement | null' is not assignable to type 'void | (() => VoidOrUndefinedOnly)'.
      Type 'null' is not assignable to type 'void | (() => VoidOrUndefinedOnly)'.
src/legacy.tsx(43,12): error TS2339: Property 'render' does not exist on type 'typeof import("C:/Users/user/react-upgrade-lab/node_modules/@types/react-dom/index")'.
$ npx vitest run
 FAIL  src/legacy.test.tsx > renders the app and lists what React warns about
TypeError: default.render is not a function
 ❯ mount src/legacy.tsx:43:12
     41|
     42| export function mount(container: HTMLElement) {
     43|   ReactDOM.render(<App />, container);
       |            ^

Rehberin önerdiği TypeScript codemod'unu çalıştırdık:

$ npx types-react-codemod@3.5.3 preset-19 ./src --yes
Results: 
0 errors
1 unmodified
0 skipped
1 ok
$ git diff src/legacy.tsx
@@ -25,12 +25,14 @@
 
 export function App() {
   const search = useRef<HTMLInputElement>(null);
-  const renders = useRef<number>();
+  const renders = useRef<number>(undefined);
   renders.current = (renders.current ?? 0) + 1;
   let box: HTMLDivElement | null = null;
   return (
     <ThemeContext.Provider value="dark">
-      <div ref={(node) => (box = node)} onClick={() => box?.focus()}>
+      <div ref={node => {
+        (box = node);
+      }} onClick={() => box?.focus()}>
         <BookTitle title="Dune" />
         <PriceTag price={489} />
         <SearchBox ref={search} placeholder="Search" />

İkinci sorunun cevabı: beşten ikisi. Codemod useRef'e argüman ekledi ve değer döndüren ref callback'ini blok gövdeye çevirdi; ReactDOM.render, string ref ve refs ise tip değil API değişikliği. Onlar için rehber npx codemod@latest react/19/migration-recipe'i öneriyor; biz o aracı çalıştırmadık, kalanları elle düzelttik:

import { Component, createContext, createRef, useContext, useRef, type Ref } from 'react';
import { createRoot } from 'react-dom/client';

const ThemeContext = createContext('light');

// defaultProps yerine varsayılan parametre
function PriceTag({ price, currency = 'TRY' }: { price: number; currency?: string }) {
  const theme = useContext(ThemeContext);
  return <span className={theme}>{price} {currency}</span>;
}

// ref sıradan bir props: forwardRef yok
function SearchBox({ ref, ...props }: { placeholder: string; ref?: Ref<HTMLInputElement> }) {
  return <input ref={ref} {...props} />;
}

// String ref yerine createRef
class BookTitle extends Component<{ title: string }> {
  heading = createRef<HTMLHeadingElement>();
  componentDidMount() {
    this.heading.current!.dataset.ready = 'yes';
  }
  render() {
    return <h2 ref={this.heading}>{this.props.title}</h2>;
  }
}

export function App() {
  const search = useRef<HTMLInputElement>(null);
  const renders = useRef<number>(undefined);
  renders.current = (renders.current ?? 0) + 1;
  let box: HTMLDivElement | null = null;
  return (
    <ThemeContext value="dark">
      <div ref={(node) => { box = node; }} onClick={() => box?.focus()}>
        <BookTitle title="Dune" />
        <PriceTag price={489} />
        <SearchBox ref={search} placeholder="Search" />
      </div>
    </ThemeContext>
  );
}

export function mount(container: HTMLElement) {
  createRoot(container).render(<App />);
}
$ npx tsc --noEmit && npx vitest run --silent=false        # react@19.3.1
stdout | src/legacy.test.tsx > renders the app and lists what React warns about
<div><h2 data-ready="yes">Dune</h2><span class="dark">489 TRY</span><input placeholder="Search"></div>
(no warnings)

createRoot render'ı senkron yapmadığı için testte mount'u act() içine almak gerekti. Kontrol listesi:

  • Önce 18.3'e geç, uyarıları sıfırla; StrictMode açık olsun.
  • createRoot / hydrateRoot'a geç; testlerde act'i react'ten import et.
  • Yeni JSX (JavaScript içinde HTML'e benzeyen syntax) transform'unu kullandığından emin ol; React 19'da zorunlu.
  • React ve tiplerini birlikte yükselt; npx types-react-codemod@latest preset-19 çalıştır, kalan tip hatalarını elle düzelt.
  • string ref, legacy context, function component defaultProps, propTypes, findDOMNode ve react-test-renderer kullanımlarını kaldır.
  • forwardRef ve <Context.Provider> hâlâ çalışıyor; yeni kodda ref props'unu ve <Context>'i kullan, eskileri fırsat buldukça taşı.
  • Kütüphanelerin React 19 desteğini kontrol et; React Router'da viewTransition yerine <ViewTransition>'a geç.

Üretimde Dikkat

Yükseltmeyi kod değişikliklerinden ayrı bir adım olarak yap: önce 18.3, sonra 19.x, her adımda bütün testler ve en azından bir Playwright akışı. Bağımlılıkların peer dependency'leri yükseltmenin en yavaş kısmı olabilir; React 19'u desteklemeyen bir kütüphane bütün uygulamayı bekletir. React Compiler'ı aynı anda açma; önce yükseltmeyi, sonra compiler'ı ayrı bir değişiklik olarak ölç (on ikinci yazı). <ViewTransition> tarayıcının View Transition API'sine dayanıyor; eklediğin animasyonları kullanıcının prefers-reduced-motion tercihine göre kısmayı unutma.

Sık Düşülen Tuzaklar

18.3'ü atlamak: doğrudan 19'a geçince uyarı yerine hata alırsın ve hangisinin hangi değişiklikten geldiğini ayırmak zorlaşır.

Codemod'un her şeyi düzelttiğini varsaymak: çıktıda gördüğümüz gibi tip codemod'u API değişikliklerine dokunmuyor; codemod sonrası tsc, testler ve uygulamanın kendisi tekrar çalıştırılmalı.

Animasyonu acil güncellemelerden beklemek: <ViewTransition> yalnız transition, Suspense reveal ve useDeferredValue güncellemelerinde çalışır; onClick içinde doğrudan setState animasyon üretmez.

useId değerinin biçimine güvenmek: 19.1'de değişti, yine değişebilir; onu yalnız id ve aria-* bağlantısı için kullan.

Küçük Bir Deneme

★☆☆ RootLayout'taki <ViewTransition>'a default="none" ver ve yalnız kitap sayfasına giderken animasyon olmasını sağla: kitap linkine tıklarken addTransitionType('details') kullanmak için ne değişmeli?

★★☆ RecentlyViewed listesini bir Fragment ile sar ve observeUsing ile bir IntersectionObserver bağla: liste ekrana girdiğinde konsola yazsın. Wrapper div eklemeden çalıştığını DOM'da kontrol et.

★★★ upgrade-lab'e react-dom/test-utils'ten act import eden ve findDOMNode kullanan bir component ekle, 18.3'te uyarıları, 19.3'te hataları gör ve react/19/migration-recipe codemod'unun bunları nasıl değiştirdiğini kendi makinende dene.

Seri burada bitiyor. Angular bilen biri olarak başladığımız yerden, Bookstore'un katalog, sepet, checkout ve stok ekranlarını React 19.3 ile yazdık; state'i, effect'i, veri çekmeyi, routing'i, form'ları, global state'i, performansı, testi, deploy'u ve sunucu tarafını aynı uygulama üzerinde gördük. Bundan sonraki adım senin projen: bir React codebase'ine katıldığında ilk iş, bu seride tek tek verdiğimiz kararların o ekipte nasıl verildiğini bulmak olacak.