JSX ve Component'ler: Template Yerine JavaScript, Props, children ve Liste Render'ı
JSX bir template dili değil, build sırasında _jsx() çağrılarına derlenen JavaScript'tir; koşul için &&, ternary ve erken return, liste için map ve verinin kendi kimliğinden gelen key kullanılır. Bookstore'un BookCard, BookList ve Layout component'lerinde props'u tipliyor, children ile ng-content'in karşılığını kuruyor; key={index}'in notu yanlış kitaba taşıdığını, 0 && ifadesinin ekrana 0 yazdığını ve dangerouslySetInnerHTML'in onerror kodunu çalıştırdığını gerçek koşularla gösteriyoruz.
Sürüm notu: Bu yazıdaki örnekler React 19.3, TypeScript 7.0, Vite 8.3, Oxlint 1.93 ve Node.js 24 ile denenmiştir; tarayıcı davranışı Playwright 1.65 ile headless Chromium'da, production build üzerinde gözlenmiştir.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) React istemcisinde üç kitaplık bir liste var: Dune, The Hobbit, Emma. Her satırda kitabın adı ve kullanıcının not yazabileceği bir <input> duruyor. Listenin üstünde "Reverse" butonu, sırayı ters çeviriyor. Satırlar books.map(...) ile üretiliyor ve her satıra bir key veriliyor.
<ul>
{books.map((book, index) => (
<li key={useIndexKey ? index : book.isbn}>
{book.title} <input aria-label={`Note for ${book.title}`} placeholder="Note" />
</li>
))}
</ul>
Kullanıcı Dune satırına "gift for Ada" yazıp "Reverse"e basıyor. Liste Emma, The Hobbit, Dune sırasına geçiyor. Soru: not hangi kitabın yanında görünür? key={book.isbn} ile key={index} arasında fark var mı? Angular'da @for (book of books; track $index) yazsaydın ne olurdu? Cevap, liste bölümünde iki koşunun gerçek çıktısında.
Bu yazıda React'in template yerine kullandığı JSX'i (JavaScript içine yazılan, HTML'e benzeyen syntax), props'u, children ile composition'ı, koşullu render'ı ve liste render'ını ele alıyoruz. Örnekler serinin uygulamasında: BookCard, BookList ve Layout.
JSX Neye Derlenir?
JSX tarayıcıya hiç ulaşmıyor; build sırasında düz JavaScript fonksiyon çağrılarına çevriliyor. Küçük bir component'i Vite'ın kullandığı Oxc dönüşümünden geçirdik. Girdi:
export function BookTitle({ title, year }: { title: string; year: number }) {
return (
<h2 className="title">
{title} {year < 1900 && <span>Classic</span>}
</h2>
);
}
Çıktı:
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
export function BookTitle({ title, year }) {
return /* @__PURE__ */ _jsxs("h2", {
className: "title",
children: [
title,
" ",
year < 1900 && /* @__PURE__ */ _jsx("span", { children: "Classic" })
]
});
}
Her JSX element'i bir _jsx ya da _jsxs çağrısı oluyor: ilk argüman tag'in adı, ikincisi props object'i. Child'lar children adlı bir prop'a giriyor. TypeScript tipleri de silinmiş. Bu çağrılar DOM (tarayıcının sayfayı tuttuğu node ağacı) üretmiyor; React'in "şu an ekranda ne olmalı" tarifini taşıyan sıradan object'ler döndürüyor. DOM'u bu tarife göre React DOM güncelliyor.
Bu dönüşüm JSX'in kurallarını da açıklıyor. Bir component tek bir değer döndürebilir; iki kardeş element'i yan yana döndürmek için onları bir element'e ya da bir Fragment'e (<>...</>) sarman gerekiyor. Fragment DOM'da iz bırakmıyor; Angular'daki ng-container'ın karşılığı gibi düşünebilirsin. Attribute'lar props object'inin key'leri olduğu için JavaScript isimleri geçerli: class yerine className, for yerine htmlFor, stroke-width yerine strokeWidth. Süslü parantez { } içine her JavaScript ifadesi yazılabiliyor; if ya da for gibi statement'lar yazılamıyor, çünkü sonuç bir fonksiyon argümanına giriyor.
Angular template'i ise kendi diliyle derleniyor: @if, @for, pipe'lar ve [property] binding'leri Angular derleyicisinin anladığı yapılar. React'te bunların hepsinin karşılığı JavaScript'in kendisi: &&, ternary, map ve fonksiyon çağrısı.
Props ve TypeScript
Serinin kartı bu yazının sonunda şöyle:
import type { BookSummary } from '@/features/catalog/types';
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) {
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}
{book.publishedYear < 1900 && <span className="badge">Classic</span>}
</span>
<strong>{price.format(book.price)}</strong>
<button type="button" onClick={() => onAdd(book.isbn)}>{label}</button>
</li>
);
}
Props'un tipi düz bir TypeScript tipi. Angular'daki input.required<BookSummary>()'un karşılığı tipte ? olmayan bir alan; kartı book vermeden kullanmaya çalışırsan tsc -b derlemeyi durduruyor. Opsiyonel bir prop için alanın sonuna ? koyuyorsun, varsayılan değeri de destructuring içinde veriyorsun: { quantity = 0 }.
react.dev props'u "immutable" ve her render için yeni bir snapshot olarak tanımlıyor: component kendi props'unu değiştirmiyor. Kart sepetteki adedi bilmek istiyor ama onu tutmuyor; değiştirmek istediğinde onAdd ile parent'a haber veriyor ve parent yeni bir quantity ile onu tekrar render ediyor. Angular'da bir input() signal'ına component içinden yazamadığın gibi, React'te de props'a yazmıyorsun. Bu kural "component'i saf tut" ilkesinin parçası: aynı props ile çağrılan component aynı JSX'i döndürmeli.
Bir not: price formatter'ı component'in dışında, module seviyesinde. Component gövdesindeki her satır her render'da çalıştığı için değişmeyen ve oluşturması maliyetli bir object'i dışarıda tutmak doğru yer.
children ile Composition: ng-content'in Karşılığı
Angular'da bir layout component'i içine konan içeriği <ng-content /> ile yerleştirir; birden fazla yer için select kullanırsın. React'te içerik children adlı özel bir prop olarak geliyor; ek "slot"lar ise sıradan props:
import type { ReactNode } from 'react';
type LayoutProps = {
title: string;
// İsimli bir ng-content slot'u gibi: header'a ne konacağına çağıran karar verir
headerExtra?: ReactNode;
children: ReactNode;
};
export function Layout({ title, headerExtra, children }: LayoutProps) {
return (
<>
<header className="site-header">
<strong>{title}</strong>
{headerExtra}
</header>
<main>{children}</main>
</>
);
}
ReactNode, JSX'in içine konabilecek her şeyin tipi: element, string, number, null, bunların array'i. headerExtra isimli bir slot gibi çalışıyor; Angular'daki <ng-content select="[header-extra]" /> karşılığı. Layout'u kullanan App:
import { useState } from 'react';
import { CartSummary } from '@/features/cart/CartSummary';
import { BookList } from '@/features/catalog/BookList';
import { sampleBooks } from '@/features/catalog/sample-books';
import { Layout } from '@/shared/Layout';
export default function App() {
// isbn -> adet; neden her seferinde yeni bir object kurduğumuzu sonraki yazı anlatıyor
const [quantities, setQuantities] = useState<Record<string, number>>({});
const itemCount = Object.values(quantities).reduce((sum, q) => sum + q, 0);
function handleAdd(isbn: string) {
setQuantities((current) => ({ ...current, [isbn]: (current[isbn] ?? 0) + 1 }));
}
return (
<Layout title="Bookstore" headerExtra={<CartSummary itemCount={itemCount} />}>
<h1>Catalog</h1>
<BookList books={sampleBooks} quantities={quantities} onAdd={handleAdd} />
</Layout>
);
}
Açılıp kapanan tag arasına yazılan her şey children oluyor; headerExtra'ya ise bir JSX element'i değer olarak veriliyor. React'te JSX bir değer olduğu için bir değişkene atanabiliyor, prop olarak geçilebiliyor, bir fonksiyondan döndürülebiliyor. Angular'da template parçasını dışarı vermek için TemplateRef ve ngTemplateOutlet gerekirken React'te bu doğal bir işlem.
Composition, React'te kalıtımın yerini tutuyor. Bir "BaseCard" class'ından türetmek yerine küçük component'leri birbirinin içine koyuyorsun. Container ve presentational component ayrımını, UI Mimari Pattern'leri serisindeki "Component Architecture: Container ve Presentational Component'ler, Composition ve Contract'lar" yazısı Angular üzerinden anlattı; aynı fikirleri React'te state'i nerede tutacağımızı konuştuğumuz beşinci yazıda kullanacağız.
Koşullu Render: @if Yerine && ve Ternary
Kartta 1900'den önce basılmış kitaplara bir "Classic" rozeti ekledik: {book.publishedYear < 1900 && <span className="badge">Classic</span>}. JavaScript'te a && b, a falsy ise a'yı, değilse b'yi döndürür. React false, null, undefined ve true değerlerini ekrana hiçbir şey çizmeden geçiyor; bu yüzden koşul yanlışsa rozet görünmüyor. İki dal gerekiyorsa ternary kullanılıyor; kartın buton etiketi böyle.
Uzun bir koşul zinciri için JSX'in içinde kalmak zorunda değilsin. BookList boş liste durumunu fonksiyonun başında erken bir return ile ele alıyor; Angular'daki @for ... @empty bloğunun karşılığı:
import { BookCard } from '@/features/catalog/BookCard';
import type { BookSummary } from '@/features/catalog/types';
type BookListProps = {
books: BookSummary[];
quantities: Record<string, number>;
onAdd: (isbn: string) => void;
};
export function BookList({ books, quantities, onAdd }: BookListProps) {
if (books.length === 0) {
return <p className="muted">No books match your search.</p>;
}
return (
<ul className="book-grid">
{books.map((book) => (
// key: @for'daki track gibi sabit bir kimlik; asla array index'i değil
<BookCard key={book.isbn} book={book} quantity={quantities[book.isbn] ?? 0} onAdd={onAdd} />
))}
</ul>
);
}
&&'in bir tuzağı var: sol taraf 0 ise React 0'ı bir sayı olarak ekrana yazıyor, çünkü 0 falsy olsa da false değil. Sepetteki ürün sayısıyla denedik:
export default function ZeroDemo() {
const itemCount = 0;
return (
<main>
<p id="wrong">Cart {itemCount && <span>({itemCount} items)</span>}</p>
<p id="right">Cart {itemCount > 0 && <span>({itemCount} items)</span>}</p>
</main>
);
}
#wrong: "Cart 0"
#right: "Cart"
Sepet boşken ilk paragraf "Cart 0" gösteriyor. Sol tarafı her zaman gerçek bir boolean yapmak (itemCount > 0) bu hatayı ortadan kaldırıyor.
Liste Render'ı ve key: @for track'in Karşılığı
BookList içindeki books.map(...) her kitap için bir BookCard element'i üretiyor. key, React'in bir render'daki element ile bir sonraki render'daki element'i eşleştirmek için kullandığı kimlik. Angular 17 ile gelen @for'daki track ifadesi neyse key de o; Angular'da track zorunlu, React'te key eksikse geliştirmede konsola uyarı düşüyor, Oxlint'in jsx-key kuralı da hatayı build'den önce yakalıyor.
Key vermeden üç başlık render eden bir listede geliştirme konsolu:
error: Each child in a list should have a unique "key" prop.%s%s See https://react.dev/link/warning-keys for more information.
Production build'de sayfanın ilk hâli ve iki kitap ekledikten sonraki header:
Dune | Frank Herbert · 1965 | ₺489.00 | Add to cart
The Hobbit | J.R.R. Tolkien · 1937 | ₺329.90 | Add to cart
Emma | Jane Austen · 1815Classic | ₺249.90 | Add to cart
Middlemarch | George Eliot · 1871Classic | ₺389.00 | Add to cart
1984 | George Orwell · 1949 | ₺279.00 | Add to cart
header: Bookstore | Cart: 3 item(s)
Emma button: In cart: 2
Şimdi "Önce Tahmin Et"teki deneme. Dune satırına "gift for Ada" yazıp "Reverse"e bastıktan sonra her satırdaki input'un değeri:
key={book.isbn} Emma="" The Hobbit="" Dune="gift for Ada"
key={index} Emma="gift for Ada" The Hobbit="" Dune=""
key={book.isbn} ile not Dune'un yanında kaldı. key={index} ile not, listenin en başına geçen Emma'nın yanına taşındı. Sebep şu: React sırayla eşleştiriyor; ters çevirmeden önce index 0 Dune'du, sonra Emma. Key aynı (0) olduğu için React bunu "aynı satır, sadece metni değişti" diye yorumladı: metni güncelledi, ama input'u ve onun DOM'daki değerini olduğu gibi bıraktı. Satırın kendi state'i olsaydı (bir sonraki yazıda kartlara ekleyeceğimiz adet seçici gibi) o da yanlış kitaba geçecekti. Angular'da track $index aynı hatayı üretir.
Kural basit: key, verinin kendi sabit kimliği olmalı. Bookstore'da kitap için ISBN (kitabın uluslararası standart numarası), sipariş için sipariş numarası. Index yalnız liste hiç sıralanmıyor, filtrelenmiyor ve araya eleman eklenmiyorsa güvenli; bu şartların ileride bozulmayacağından emin olmak zor. Math.random() ya da crypto.randomUUID() ile render sırasında key üretmek ise daha kötü: her render'da bütün satırlar yeniden oluşturuluyor. Projeye eklediğimiz no-array-index-key kuralı index kullanımını işaretliyor:
src/App.tsx:7:13: warning react(no-array-index-key): Usage of Array index in keys is not allowed
help: Use a unique data-dependent key to avoid unnecessary rerenders
XSS: JSX Neyi Escape Eder, dangerouslySetInnerHTML Neyi Açar?
JSX'in { } içine konan string'leri HTML olarak değil, metin olarak DOM'a yazması önemli bir güvenlik özelliği. Bir kullanıcının yazdığı kitap yorumunu iki şekilde gösterdik:
// Bir kullanıcının yazdığı yorum; API'den geldiğini düşün
const review = 'Great book <img src="x" onerror="console.log(\'XSS ran on \' + location.host)">';
export default function XssDemo() {
return (
<main>
<p id="escaped">{review}</p>
<p id="raw" dangerouslySetInnerHTML={{ __html: review }} />
</main>
);
}
console: XSS ran on 127.0.0.1:52143
#escaped innerHTML: Great book <img src="x" onerror="console.log('XSS ran on ' + location.host)">
#raw innerHTML: Great book <img src="x" onerror="console.log('XSS ran on ' + location.host)">
İlk paragrafta yorum metin olarak kaldı: <img karakter karakter ekrana yazıldı ve hiçbir şey çalışmadı. İkincisinde dangerouslySetInnerHTML string'i innerHTML'e verdi, tarayıcı <img>'i oluşturdu, resim yüklenemeyince onerror çalıştı ve konsola sayfanın adresini yazdı. Gerçek bir saldırıda bu satır cookie okuyan ya da kullanıcı adına istek atan bir kod olurdu. Prop'un adındaki "dangerously" tam olarak bunu söylüyor: Angular'daki bypassSecurityTrustHtml gibi, React'in koruması bu noktada devre dışı.
Oxlint'in no-danger kuralını warning olarak açtık; kullanım code review'da görünür oluyor:
src/App.tsx:8:19: warning react(no-danger): Do not use `dangerouslySetInnerHTML` prop
help: `dangerouslySetInnerHTML` is a way to inject HTML into your React component. This is dangerous because it can easily lead to XSS vulnerabilities.
exit code: 0
HTML göstermek gerçekten gerekiyorsa (bir CMS'ten (içerik yönetim sistemi) gelen kitap tanıtımı gibi) içerik DOMPurify gibi bir sanitizer'dan geçmeli, mümkünse sunucuda. React 19.3 ile tarayıcının Trusted Types API'si de destekleniyor: React artık TrustedHTML object'lerini string'e çevirmeden DOM'a veriyor, böylece require-trusted-types-for 'script' içeren bir CSP (Content Security Policy; tarayıcıya hangi kaynakların çalıştırılabileceğini söyleyen header) altında policy'ler gerçekten çalışabiliyor. Bu konuların ayrıntısı Frontend Güvenliği serisinde: "Trusted Types'ı Production'a Almak: Angular Policy'leri, Üçüncü Parti Kütüphaneler ve Baseline 2026". JSX'in escape'i tek başına yeterli değil; href'e kullanıcıdan gelen bir URL koymak gibi başka sink'ler de var ve bunlar aynı seride anlatılıyor.
Üretimde Dikkat
Liste key'lerini API'nin döndürdüğü kimlikten al. Bookstore API kitaplar için ISBN'i, sepet satırları için de ISBN'i döndürüyor; key üretmek için ayrı bir sayaç tutmaya gerek yok. Kimliği olmayan veri (ör. bir formda kullanıcının eklediği ama henüz kaydedilmemiş satırlar) için kimliği satır oluşturulurken bir kez üret (crypto.randomUUID()) ve state'te sakla; render sırasında üretme.
Büyük listelerde (binlerce kitap) her öğeyi DOM'a koymak yavaşlar; bunun çözümü virtualization ve performans yazısının konusu. Key'i doğru vermek orada da ilk şart: React yeniden sıralanan bir listede DOM node'larını yeniden oluşturmak yerine taşıyabiliyorsa bunu key sayesinde yapıyor.
dangerouslySetInnerHTML kullanımlarını lint ile görünür tut ve her birinin yanında içeriğin nereden geldiğini, nerede temizlendiğini yaz. Bir component kütüphanesi seçerken de ona bak: bazı rich text ve markdown bileşenleri bu prop'u içeride kullanıyor.
Sık Düşülen Tuzaklar
Key'i yanlış yere koymak: key, map'in döndürdüğü en dıştaki element'e verilir. Kartın içindeki <li>'ye koymak işe yaramaz, çünkü React kardeşleri BookList'teki <BookCard> seviyesinde karşılaştırıyor. Key bir prop gibi görünse de component'e ulaşmıyor; BookCard içinde props.key okunamaz.
&&'in solunda bir sayı ya da string bırakmak: yukarıdaki "Cart 0" hatası. items.length && ... yerine items.length > 0 && ... yaz.
Render sırasında props'u ya da dışarıdaki bir array'i değiştirmek: books.sort(...) parent'ın array'ini yerinde sıralar ve bir sonraki render'da beklenmedik sonuçlar verir. Sıralanmış bir kopya için toSorted kullan; bunu dördüncü yazıda göreceğiz.
Component'i başka bir component'in içinde tanımlamak: App fonksiyonunun içinde function Row() {...} yazarsan her render'da yeni bir component tipi oluşur ve React o alt ağacın state'ini her seferinde sıfırlar. Component'ler module seviyesinde tanımlanır.
Küçük Bir Deneme
★☆☆ BookCard'a category değerini gösteren bir satır ekle; kategori "Science Fiction" ise yanına küçük bir SF rozeti çıksın. Bunu bir kez && ile, bir kez ternary ile yaz.
★★☆ Layout'a footer adlı opsiyonel bir slot ekle. Verilmediğinde varsayılan olarak "Bookstore · 2026" yazsın. Angular'da aynı şeyi ng-content ile nasıl yapardın?
★★★ "Önce Tahmin Et"teki listeye "Shuffle" butonu ekle ve key={index} ile birkaç kez karıştır. Not hangi satırlarda kalıyor? Ardından input'u kontrolsüz bırakmak yerine bir state'e bağlamayı düşün: index key'li listede bu hatayı tek başına çözer mi?
Kart artık bir sayıyı gösteriyor ama sayıyı değiştiren tek şey App'teki bir satır. Sıradaki yazıda event'ler, useState, state'in render başına bir snapshot olması, updater fonksiyonu ve React'in bir component'i ne zaman yeniden render ettiğini gerçek render log'larıyla göreceğiz.