Angular Geliştiricisi için React: Library mi Framework mü, Zihinsel Model Farkları ve Ekosistem Haritası
React bir framework değil, UI library'si: component, state ve render'ı veriyor; router, veri çekme, form ve global state'i ekip ekosistemden seçiyor. Bookstore'un kitap kartını Angular ve React'te yan yana yazıyor, bir tıklamada React'in component fonksiyonunu yeniden çağırıp DOM'da yalnız iki text node'u değiştirdiğini gerçek çıktıyla gösteriyor, Angular kavramlarının React karşılıklarını tablo hâlinde veriyoruz.
Sürüm notu: Bu yazıdaki React örnekleri React 19.3, TypeScript 7.0, Vite 8.3 ve Node.js 24 ile denenmiştir; Angular karşılaştırma kodu Angular 22.2 ile (Angular 22.2 projeleri TypeScript 6.0 kullanıyor) derlenip çalıştırılmıştır. Tarayıcı davranışı Playwright 1.65 ile headless Chromium'da gözlenmiştir.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) Angular istemcisinde, katalogdaki her kitap bir kart olarak görünüyor: başlık, yazar, yıl, fiyat ve bir "Add to cart" butonu. Bu kartı React ile yazıyoruz ve butona iki kez tıklıyoruz. Kartın kendisi sepetteki adedi bilmiyor; adedi üstteki component tutuyor ve karta bir sayı olarak veriyor.
İki soru: Birinci tıklamadan sonra React, kartı çizen fonksiyonu kaç kez çağırır ve bu çağrı sırasında hangi DOM (tarayıcının sayfayı tuttuğu ağaç yapısı) node'ları değişir? Angular'da aynı kart input() ve signal ile yazıldığında tıklamadan sonra ne çalışıyor? Cevabı tahmin et; yazının ortasında iki uygulamanın gerçek çıktısıyla karşılaştıracağız.
Bu seri Angular bilen bir geliştiricinin React'i öğrenmesi için yazıldı. Her yazıda "Angular'da bunu nasıl yapıyordum?" sorusundan başlayıp React'teki karşılığına geçeceğiz. İlk yazının amacı kod yazmaktan çok zihinsel modeli kurmak: React neyi veriyor, neyi vermiyor ve iki dünyanın parçaları birbirine nasıl denk geliyor.
React Kendini Neden "Library" Diye Tanıtıyor?
react.dev'in ana sayfası React'i "The library for web and native user interfaces" diye tanıtıyor. Bu ifade pazarlama cümlesi değil, gerçek bir kapsam tanımı. React'in çekirdeği üç şeyle ilgileniyor: component'leri tanımlamak, onların state'ini tutmak ve state değiştiğinde ekranı güncellemek. Routing, HTTP istekleri, form doğrulama, dependency injection ya da build sistemi React paketinin içinde yok.
Angular'da durum tam tersi. "Angular Nedir, Neden Var? Bir Framework'ün Varlık Sebebi" yazısında anlattığımız gibi Angular bir uygulamanın ihtiyaç duyduğu parçaların çoğunu tek pakette, birbirine uyumlu sürümlerle veriyor: @angular/router, HttpClient, Reactive Forms, Signal Forms, dependency injection (DI), CLI, test altyapısı. Ekip "hangi router'ı kullanalım?" tartışmasına girmez; Angular'ın router'ı vardır.
React'te bu kararlar ekibe kalıyor. react.dev'in "Build a React App from Scratch" sayfası bunu açıkça yazıyor: Vite, Parcel ya da Rsbuild gibi bir build aracıyla başlayan bir proje routing, data fetching ve styling için hazır bir çözüm içermez, bunları ekip seçer. Aynı doküman yeni bir uygulama için Next.js ya da React Router'ın framework modu gibi bir framework ile başlamayı öneriyor. Create React App ise Şubat 2025'te yeni projeler için deprecated ilan edildi, artık bir seçenek değil.
Bu farkın pratik sonucu şu: Angular projesinden React projesine geçtiğinde ilk hafta "React nasıl yazılır" kadar "bu projede hangi kütüphaneler seçilmiş ve neden" sorusuyla da uğraşırsın. Bu seride kararlarımızı baştan veriyoruz: Vite ile bir SPA (Single Page Application; tüm sayfaların tek bir HTML üzerinden tarayıcıda çizildiği uygulama), React Router 8, veri için TanStack Query, formlar için react-hook-form ve zod, global state için gerektiğinde Zustand. Next.js'i serinin sonunda tek bir yazıda konumlandıracağız.
UI = f(state): Aynı Kitap Kartı İki Kez
React'in zihinsel modeli tek bir formüle sığar: ekran, state'in bir fonksiyonudur. Bir component, aldığı props ve kendi state'i ile "şu an ekranda ne olmalı?" sorusunu cevaplayan bir JavaScript fonksiyonu. State değişince React o fonksiyonu yeniden çağırır, yeni sonucu eskisiyle karşılaştırır ve yalnızca farkı DOM'a yazar.
Önce Angular tarafı. Bu kart, Bookstore deposundaki Angular istemcisinin (bookstore-web) bir kopyasına eklenip derlendi:
import { CurrencyPipe } from '@angular/common';
import { Component, computed, input, output } from '@angular/core';
import { BookSummary } from '../core/api-models';
@Component({
selector: 'app-book-card',
imports: [CurrencyPipe],
template: `
<li class="book-card">
<strong>{{ book().title }}</strong>
<span class="muted">{{ book().author }} · {{ book().publishedYear }}</span>
<strong>{{ book().price | currency: 'TRY' : 'symbol-narrow' }}</strong>
<button type="button" (click)="add.emit(book().isbn)">{{ label() }}</button>
</li>
`,
})
export class BookCard {
readonly book = input.required<BookSummary>();
readonly quantity = input(0);
readonly add = output<string>();
// Türetilmiş değer: yalnız quantity değişince yeniden hesaplanır
protected readonly label = computed(() => (this.quantity() === 0 ? 'Add to cart' : `In cart: ${this.quantity()}`));
}
Angular'da component bir class. Girdiler input(), olaylar output(), türetilmiş değer computed(). Template ayrı bir dilde yazılıyor: {{ }} interpolation, (click) event binding, | currency pipe. Kartı kullanan component adedi bir signal içinde tutuyor ve (add) olayında quantity.update(q => q + 1) çağırıyor.
Şimdi aynı kart React'te:
// Bookstore.Api'nin döndürdüğü BookSummary ile aynı şekil (GET /api/books).
export type BookSummary = {
isbn: string;
title: string;
author: string;
category: string;
price: number;
publishedYear: number;
};
type BookCardProps = {
book: BookSummary;
quantity: number;
onAdd: (isbn: string) => void;
};
const price = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'TRY', currencyDisplay: 'narrowSymbol' });
export function BookCard({ book, quantity, onAdd }: BookCardProps) {
// Türetilmiş değer: her render'da hesaplanır, hiçbir yerde saklanmaz
const label = quantity === 0 ? 'Add to cart' : `In cart: ${quantity}`;
return (
<li className="book-card">
<strong>{book.title}</strong>
<span className="muted">{book.author} · {book.publishedYear}</span>
<strong>{price.format(book.price)}</strong>
<button type="button" onClick={() => onAdd(book.isbn)}>{label}</button>
</li>
);
}
Component bir fonksiyon. Girdiler fonksiyonun parametresi olan props object'inden geliyor; TypeScript tipi BookCardProps ile hangi props'un zorunlu olduğunu derleyici biliyor. Angular'daki output() yerine bir callback prop var: onAdd. Kart bir olay yayınlamıyor, kendisine verilen fonksiyonu çağırıyor. label bir computed değil, fonksiyonun içinde sıradan bir const: fonksiyon her çağrıldığında yeniden hesaplanıyor. Template yok; dönen değer JSX (JavaScript'in içine gömülmüş, HTML'e benzeyen bir syntax). Fiyatı biçimlendirmek için pipe yerine doğrudan Intl.NumberFormat kullanıyoruz.
Kartı kullanan component:
import { useState } from 'react';
import { BookCard, type BookSummary } from './BookCard.tsx';
const dune: BookSummary = {
isbn: '9780441172719',
title: 'Dune',
author: 'Frank Herbert',
category: 'Science Fiction',
price: 489,
publishedYear: 1965,
};
export default function App() {
// State: burada zamanla değişen tek değer
const [quantity, setQuantity] = useState(0);
return (
<main>
<h1>Bookstore</h1>
<p className="muted">Cart: {quantity} item(s)</p>
<ul className="book-grid">
<BookCard book={dune} quantity={quantity} onAdd={() => setQuantity((q) => q + 1)} />
</ul>
</main>
);
}
useState(0) bir çift döndürüyor: o anki değer ve onu değiştiren fonksiyon. Angular'daki signal(0) ile quantity.update(...) ikilisinin karşılığı bu. Fark şurada: quantity bir signal değil, düz bir sayı. Onu okumak için fonksiyon çağırmıyoruz, çünkü React değişikliği okumaları izleyerek değil, setQuantity çağrısından öğreniyor.

Bir Tıklamada Ne Oluyor? Change Detection ile Re-render Farkı
İki uygulamayı Playwright ile açıp butona iki kez tıkladık. React tarafının çıktısı:
initial | Cart: 0 item(s) | Add to cart
after 1 click | Cart: 1 item(s) | In cart: 1
after 2 clicks | Cart: 2 item(s) | In cart: 2
Angular tarafı:
initial | Cart: 0 item(s) | Add to cart | ₺489.00
after 1 click | Cart: 1 item(s) | In cart: 1 | ₺489.00
after 2 clicks | Cart: 2 item(s) | In cart: 2 | ₺489.00
Ekrandaki sonuç aynı; arkadaki mekanizma farklı. Angular 22'de zoneless change detection ve OnPush varsayılan ("Angular 21: Varsayılan Zoneless, Deneysel Signal Forms ve Vitest" ve "Angular 22: Varsayılan OnPush, Stable Signal Forms ve Angular 12 ile Proje Farkı" yazıları). quantity.update çağrısı signal'ı değiştiriyor, Angular o signal'ı okuyan template'leri dirty olarak işaretliyor ve bir sonraki change detection turunda yalnızca o binding'leri güncelliyor. Component class'ının constructor'ı tekrar çalışmıyor; class bir kez oluşturuluyor ve yaşamaya devam ediyor.
React'te ise setQuantity çağrısı bir render tetikliyor. React App fonksiyonunu yeniden çağırıyor; App yeni bir JSX ağacı döndürüyor ve içindeki BookCard için kart fonksiyonu da yeniden çağrılıyor. Yani "Önce Tahmin Et"teki ilk sorunun cevabı: bir tıklamada BookCard fonksiyonu bir kez çalışıyor (geliştirme modunda StrictMode bunu bilerek iki kez yapıyor; ikinci yazıda gerçek çıktısıyla göreceğiz). Ardından React yeni ağacı öncekiyle karşılaştırıyor; bu karşılaştırmaya reconciliation deniyor. DOM'a ne yazıldığını görmek için #root'a bir MutationObserver bağlayıp tek bir tıklama yaptık:
characterData in <p>: "Cart: 1 item(s)"
characterData in <button>: "In cart: 1"
İki fonksiyon yeniden çalıştı ama DOM'da yalnız iki text node değişti: üstteki paragraf ve butonun etiketi. <li>, başlık ve fiyat node'ları olduğu gibi kaldı; yeni element oluşturulmadı.
Angular serisindeki "Angular'da Change Detection ve Zone.js: OnPush, ChangeDetectorRef ve NgZone" yazısını okuduysan şöyle düşünebilirsin: Angular'da birim, binding; React'te birim, component fonksiyonu. React bir component'i yeniden çağırdığında varsayılan olarak onun altındaki bütün component'leri de çağırır. Bu kulağa pahalı geliyor ama fonksiyonu çağırmak ile DOM'u değiştirmek aynı şey değil; pahalı olan DOM işlemleri yalnızca fark için yapılıyor. Gereksiz render'ları ölçmeyi ve React Compiler'ın bunu nasıl otomatik azalttığını performans yazısında ele alacağız.
Bu modelin önemli bir sonucu var: render fonksiyonu saf olmalı. Aynı props ve state ile çağrıldığında aynı JSX'i döndürmeli, dışarıdaki bir değişkeni değiştirmemeli, HTTP isteği atmamalı. React fonksiyonu ne zaman ve kaç kez çağıracağına kendisi karar veriyor; bu yüzden yan etkiler render'ın içinde değil, event handler'larda ya da effect'lerde yaşıyor.
Eşleme Tablosu: Angular Kavramı, React Karşılığı
Serinin geri kalanında sık döneceğimiz eşlemeler:
| Angular | React | Not |
|---|---|---|
@Component class + template | Fonksiyon component + JSX | Decorator ve ayrı template dili yok |
input() / @Input | Props | Props read-only; component onları değiştirmez |
output() / @Output | Callback prop (onAdd) | Event yayınlamak yerine verilen fonksiyonu çağırmak |
signal() | useState | Değer düz bir değişken, setter render tetikler |
computed() | Render içinde hesaplanan değer | Pahalıysa useMemo; çoğu zaman gerekmez |
effect() | useEffect | Dış sistemle senkronizasyon için; altıncı yazının konusu |
ng-content | children prop | Composition üçüncü yazıda |
| Service + DI | Custom hook + Context | Hiyerarşik injector yok |
@angular/router | React Router | Ayrı paket; seride sürüm 8 |
HttpClient, httpResource | fetch, TanStack Query | React bir HTTP istemcisi vermez |
| Reactive Forms, Signal Forms | Controlled input, form action'ları, react-hook-form | Form yazısında kıyas tablosu |
Tablo bir başlangıç noktası, birebir çeviri sözlüğü değil. En çok yanıltan satır effect: Angular'da da React'te de "bir şey değişince çalışan kod" gibi görünüyor ama React'te effect'i lifecycle hook'u gibi kullanmak en yaygın hatadır. ngOnInit karşılığı arayan biri neredeyse her zaman effect'e ihtiyaç duymayan bir problemi effect ile çözmeye çalışıyordur.
React'in Vermediği Parçalar: Ekosistem Haritası
React çekirdeği component, state, effect, context ve render'ı veriyor. Bir Bookstore uygulaması için gereken diğer parçalar ekosistemden geliyor. Bu seride kullanacağımız seçimler ve yayın günündeki sürümleri:
- Build ve dev server: Vite 8.3 (
npm create vite,react-tstemplate'i). Alternatifler: Parcel, Rsbuild. - Routing: React Router 8.5 (data mode,
createBrowserRouter). Alternatif: TanStack Router. - Server state ve veri çekme: TanStack Query 5.107. Alternatifler: SWR (Vercel'in veri çekme kütüphanesi), React Router loader'ları.
- Form ve doğrulama: React 19'un form action'ları, react-hook-form 7.92 ve zod 4.6.
- Global client state: Zustand 5.1; büyük ekiplerde Redux Toolkit 2.14.
- Test: Vitest 5.1, React Testing Library ve MSW (Mock Service Worker; API'yi network seviyesinde taklit eden kütüphane).
- Full-stack framework: Next.js 16.4 ya da React Router'ın framework modu; serinin sonunda kısa bir bakış.
Bu listedeki hiçbir şey "resmî" değil; yaygın tercihler. Angular'dan gelen biri için en önemli fark şu: bu kütüphanelerin sürümleri birbirinden bağımsız ilerliyor. Angular'da ng update bütün @angular/* paketlerini birlikte yükseltir; React projesinde React Router'ın yalnız ECMAScript module (ESM) formatında dağıtılması ya da TanStack Query'nin yeni major sürümü ayrı ayrı planlanması gereken işler.
Serinin Uygulaması: Vite ile İlk Kurulum
Seri boyunca büyüyecek uygulamanın adı bookstore-react. Bookstore'un ASP.NET Core API'sine bağlanacak, katalog, kitap detayı, sepet ve sipariş ekranlarını içerecek. İlk adım Vite'ın react-ts template'i:
npm create vite@latest bookstore-react -- --template react-ts
cd bookstore-react
npm install
npm run dev
Vite 8 Node.js 20.19 ya da 22.12 ve üstünü istiyor; seride Node.js 24 LTS (uzun süreli destek sürümü) kullanıyoruz. create-vite 9.3'ün çıktısı:
create-vite v9.3.4
│
◇ Scaffolding project in /home/user/src/bookstore-react...
│
└ Done. Now run:
cd bookstore-react
npm install
npm run dev
Template'in getirdiği demo dosyalarını (logolar, App.css) sildik ve App.tsx'i yukarıdaki kartla değiştirdik. Production build:
vite v8.3.8 building client environment for production...
transforming...
✓ 17 modules transformed.
rendering chunks...
computing gzip size...
dist/index.html 0.45 kB │ gzip: 0.29 kB
dist/assets/index-DkxHVuhS.css 0.62 kB │ gzip: 0.36 kB
dist/assets/index-WmBcMOIe.js 220.38 kB │ gzip: 68.99 kB
✓ built in 228ms
npm run build önce tsc -b ile tip kontrolü yapıyor, sonra Vite paketliyor. Template'in package.json'ında TypeScript ~7.0.2, yani Go ile yazılan yeni derleyici geliyor (TypeScript serisindeki "TypeScript 7 ve Sonrası: Go ile Yazılan Derleyici ve Yeni Varsayılanlar" yazısı); lint için de ESLint yerine Oxlint var. 220 KB'lık JavaScript'in büyük kısmı react-dom; uygulamanın kendi kodu birkaç satır. Proje yapısını, StrictMode'u ve API'ye bağlanmak için dev proxy'yi bir sonraki yazıda ele alıyoruz.
Üretimde Dikkat
React'in "sadece UI library" olması, büyük bir projede kararların dağınık verilmesi riskini getiriyor. Aynı codebase'de iki farklı veri çekme yöntemi, üç farklı form yaklaşımı görmek alışılmadık değil. Proje başında bir karar listesi tut: router, server state, form, global state, test ve stil için hangi kütüphane, hangi sürüm, neden. Angular'ın sana hazır verdiği tutarlılığı React'te bu liste sağlıyor.
Sürüm yönetimini de buna göre planla. React, React Router, TanStack Query ve Vite birbirinden bağımsız release takvimi izliyor. Dependabot ya da Renovate ile güncellemeleri küçük ve sık tutmak, yılda bir "büyük yükseltme" yapmaktan çok daha az sorun çıkarıyor.
Ekip Angular'dan geliyorsa ilk birkaç hafta en çok iki konuda hata görürsün: state'i doğrudan değiştirmek (signal'larda alışılan set/update disiplini React'te immutable güncelleme demek) ve effect'i lifecycle gibi kullanmak. Code review'da bu iki noktaya özellikle bak; React'in lint kuralları ikincisinin bir kısmını yakalıyor.
Sık Düşülen Tuzaklar
Birinci tuzak, React'i küçük bir Angular gibi düşünmek. Class component, lifecycle method'ları ve "service" arayışı seni eski React belgelerine ve eski kalıplara götürür. 2026'da React kodu fonksiyon component ve hook'larla yazılıyor; class component'ler hâlâ çalışıyor ama yeni kodda kullanılmıyor.
İkinci tuzak, render fonksiyonunu "bir kez çalışan kurulum kodu" sanmak. Component gövdesine yazdığın her satır her render'da çalışır. new Intl.NumberFormat(...) gibi değişmeyen bir şeyi bu yüzden component dışına, module seviyesine koyduk. HTTP isteği, setInterval ya da global bir değişkene yazmak gibi işleri render içinde yapmak ise doğrudan hata.
Üçüncü tuzak, eski kaynaklardan proje kurmak. İnternette hâlâ npx create-react-app ile başlayan çok sayıda içerik var; Create React App (CRA) deprecated. Yeni projede ya bir framework ya da Vite ile başla.
Dördüncü tuzak, eşleme tablosunu birebir uygulamak. React'te her service'in bir Context'e, her computed'ın bir useMemo'ya çevrilmesi gerekmiyor. Çoğu türetilmiş değer render içinde düz bir hesaplamadır; Context ise sık değişen veriler için pahalı olabilir. Bunları ilgili yazılarda ölçerek göreceğiz.
Küçük Bir Deneme
★☆☆ Kendi bookstore-react projeni Vite ile oluştur ve BookCard'a category değerini gösteren bir satır ekle. Build çıktısındaki JavaScript boyutu değişti mi?
★★☆ Kartta ikinci bir buton olsun: "Remove". Adet 0 iken buton görünmesin. Bunu Angular'da @if ile nasıl yazardın, React'te hangi JavaScript ifadesiyle yazıyorsun? Kartın yine adedi kendisi tutmadığından emin ol.
★★★ BookCard fonksiyonunun başına bir console.log koy ve production build'i (npm run build ve npm run preview) açıp butona tıkla. Kaç satır görüyorsun? Aynı şeyi npm run dev ile dene; fark nereden geliyor?
Son sorunun cevabı bir sonraki yazının konusu: proje yapısı, path alias, Oxlint'in hook kuralları, StrictMode'un geliştirmede bilerek yaptığı çift render ve Bookstore API'ye CORS'a (tarayıcının başka origin'e yapılan istekleri sınırlayan kurallar) takılmadan bağlanmak için Vite'ın dev proxy'si.