İlk React Projesi: Vite + TypeScript, Proje Yapısı, StrictMode ve React DevTools
Vite'ın react-ts template'i, feature klasörleri ve Vite 8'in resolve.tsconfigPaths ayarıyla tek kaynaktan @/ alias'ı, Oxlint'in hook kuralları ve /api isteklerini Bookstore API'ye aktaran dev proxy ile seriye yetecek bir proje iskeleti kuruyoruz. StrictMode'un geliştirmede render'ı iki kez, effect'i kurulum-temizlik-kurulum sırasıyla çalıştırdığını, production build'de ise her şeyin bir kez çalıştığını ve proxy olmadan tarayıcının CORS yüzünden yanıtı okumadığını gerçek çıktılarla 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; API çağrıları Bookstore.Api'nin (ASP.NET Core 10) yerel bir kopyasına yapılmış, tarayıcı davranışı Playwright 1.65 ile headless Chromium'da gözlenmiştir.
Önce Tahmin Et
İlk yazıda sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı) için bookstore-react projesini Vite ile oluşturduk ve bir kitap kartı yazdık. Şimdi kartın başına bir console.log ve küçük bir effect ekliyoruz:
export function BookCard({ book, quantity, onAdd }: BookCardProps) {
console.log(`render BookCard ${book.title}`);
useEffect(() => {
console.log(`effect: subscribe ${book.isbn}`);
return () => console.log(`cleanup: unsubscribe ${book.isbn}`);
}, [book.isbn]);
Sayfayı npm run dev ile açıyoruz, hiçbir şeye dokunmadan konsola bakıyoruz. Kaç satır görürsün, hangi sırayla? Sonra butona bir kez tıklıyoruz: kaç yeni satır? Aynı şeyi npm run build ve npm run preview ile açtığımızda sonuç değişir mi? Cevaplar StrictMode bölümünde, iki ortamın gerçek konsol çıktısıyla.
Bu yazıda projeyi seri boyunca büyüyebilecek bir hâle getiriyoruz: template'in getirdiği dosyalar, feature klasörleri, @/ path alias'ı, Oxlint'in hook kuralları, StrictMode, React DevTools ve Bookstore API'ye bağlanmak için Vite'ın dev proxy'si.
Neden Framework Değil de Vite?
react.dev yeni bir uygulama için bir framework ile başlamayı öneriyor: Next.js (App Router), React Router'ın framework modu ya da mobil için Expo. Bu framework'ler routing, veri yükleme, code splitting ve gerektiğinde sunucu tarafı render'ı birlikte veriyor. Aynı doküman, öğrenmek isteyenler ya da kendi altyapısını kurmak isteyenler için Vite, Parcel veya Rsbuild ile sıfırdan başlamayı da ayrı bir yol olarak anlatıyor.
Bu seride Vite ile bir SPA (Single Page Application; bütün ekranların tarayıcıda çizildiği tek sayfalık uygulama) kuruyoruz. Sebep pedagojik: Angular okuru ng new ile bir SPA kurmaya alışkın, Bookstore'un Angular istemcisi de bir SPA. Aynı mimariyi React ile kurunca iki dünyayı parça parça kıyaslayabiliyoruz; sunucu tarafı render, Server Components ve hosting farkları gibi yeni kavramlar karşılaştırmayı bulandırmıyor. Routing'i React Router 8'in data mode'u ile ekleyeceğiz; Next.js'i serinin sonunda ayrı bir yazıda ele alacağız. Gerçek bir projede bu kararı ekip verir: SEO (arama motoru optimizasyonu) gereken, ilk açılışın sunucudan gelmesi gereken bir site için framework daha doğru seçim olabilir.
Template'in Getirdikleri
npm create vite@latest bookstore-react -- --template react-ts komutunun ürettiği projenin giriş noktası index.html. İçinde tek bir <div id="root"> ve /src/main.tsx'i yükleyen bir module script var. Angular'da index.html'deki <app-root> ve main.ts'teki bootstrapApplication neyse, burada da o:
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
createRoot verilen DOM (tarayıcının sayfayı tuttuğu ağaç) node'unun içini React'e devrediyor; render ilk render'ı tetikliyor. Bundan sonra o div'in içinde olan her şey React'in sorumluluğunda. Sondaki ! TypeScript'e "getElementById burada null dönmez" demek; index.html'i biz yazdığımız için bu varsayım güvenli.
Diğer dosyalar: vite.config.ts Vite ayarları ve React plugin'i (@vitejs/plugin-react 6.3, JSX (JavaScript içine yazılan HTML benzeri syntax) dönüşümü ve Fast Refresh için Oxc kullanıyor). tsconfig.app.json uygulama kodunun, tsconfig.node.json ise vite.config.ts'in tip ayarları; kökteki tsconfig.json ikisini project reference olarak bağlıyor ve tsc -b ikisini birlikte derliyor. "jsx": "react-jsx" ayarı JSX'in otomatik runtime ile derleneceğini söylüyor; bu yüzden dosyalarda import React from 'react' satırına gerek yok. "noEmit": true çünkü JavaScript'i TypeScript değil Vite üretiyor; tsc yalnızca tip kontrolü yapıyor.
Feature Klasörleri ve Path Alias
Template her şeyi src/ altına düz koyuyor. Seri boyunca katalog, sepet, sipariş ve kimlik doğrulama ekranları ekleyeceğimiz için kodu teknik türe göre (components/, hooks/) değil, iş alanına göre ayırıyoruz. Bu yazının sonunda proje şöyle:
bookstore-react/
├── index.html
├── package.json
├── vite.config.ts
├── tsconfig.json
├── tsconfig.app.json
├── tsconfig.node.json
├── .oxlintrc.json
├── public/
│ └── favicon.svg
└── src/
├── main.tsx
├── App.tsx
├── index.css
└── features/
├── cart/
│ └── CartSummary.tsx
└── catalog/
├── BookCard.tsx
└── types.ts
Bookstore API'nin döndürdüğü JSON şekilleri features/catalog/types.ts'te. Angular istemcisindeki api-models.ts ile aynı alan adları:
// Bookstore.Api'nin döndürdüğü şekiller (System.Text.Json camelCase kullanıyor).
export type BookSummary = {
isbn: string;
title: string;
author: string;
category: string;
price: number;
publishedYear: number;
};
export type CategoryView = {
name: string;
slug: string;
};
export type PagedResult<T> = {
items: T[];
total: number;
page: number;
pageSize: number;
};
Klasörler derinleştikçe ../../features/catalog/types gibi göreli import'lar okunmaz hâle geliyor. TypeScript tarafında tsconfig.app.json'a bir paths eşlemesi ekliyoruz:
"paths": {
"@/*": ["./src/*"]
},
baseUrl yazmadık; paths içindeki yollar tsconfig dosyasının bulunduğu klasöre göre çözülüyor. Vite tarafında eskiden resolve.alias ile aynı eşlemeyi ikinci kez yazmak ya da vite-tsconfig-paths plugin'ini kurmak gerekiyordu. Vite 8'de resolve.tsconfigPaths seçeneği var; açınca Vite eşlemeyi tsconfig'ten okuyor:
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
// Dev proxy'nin gideceği Bookstore.Api adresi (README'deki varsayılan: http://localhost:5080)
const apiTarget = process.env.BOOKSTORE_API ?? 'http://localhost:5080'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
// '@/...' import'ları tsconfig.app.json'daki "paths"ten çözülür
tsconfigPaths: true,
},
server: {
proxy: {
// /api/books -> http://localhost:5080/api/books; tarayıcı için aynı origin
'/api': { target: apiTarget, changeOrigin: true },
},
},
})
Dosyanın ikinci yarısındaki server.proxy ayarını API bölümünde açıklayacağız. Artık import'lar import { BookCard } from '@/features/catalog/BookCard' şeklinde ve dosya taşındığında yalnız bu satır değişiyor. Uzantıyı yazmıyoruz; template'in kendi main.tsx'indeki ./App.tsx gibi uzantılı import'lar da allowImportingTsExtensions sayesinde çalışıyor.
Oxlint ve Hook Kuralları
create-vite 9'un React template'i lint için ESLint yerine Oxlint ile geliyor: Rust ile yazılmış, ayarı tek bir .oxlintrc.json dosyası. Template iki React kuralını açıyor; biz bunlara iki kural ekledik:
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["react", "typescript", "oxc"],
"rules": {
"react/rules-of-hooks": "error",
"react/exhaustive-deps": "warn",
"react/jsx-key": "error",
"react/only-export-components": ["warn", { "allowConstantExport": true }]
}
}
rules-of-hooks, hook'ların her render'da aynı sırada çağrılmasını denetliyor. React bir component'in state'lerini isimle değil çağrı sırasıyla eşleştirdiği için bir useState'i if'in içine ya da erken bir return'ün arkasına koymak, bir sonraki render'da state'lerin yer değiştirmesine yol açar. exhaustive-deps ise effect'in içinde kullanılan reaktif değerlerin dependency array'de olup olmadığına bakıyor. İkisini de bilerek bozan bir component:
import { useEffect, useState } from 'react';
export function CartBadge({ cartId }: { cartId: string | null }) {
if (cartId === null) {
return null;
}
// Erken return'den sonra çağrılıyor: hook'ların sırası render'dan render'a değişir
const [count, setCount] = useState(0);
useEffect(() => {
fetch(`/api/carts/${cartId}`)
.then((response) => response.json())
.then((cart: { items: unknown[] }) => setCount(cart.items.length));
}, []);
return <span>{count}</span>;
}
npm run lint çıktısı:
> bookstore-react@0.0.0 lint
> oxlint
src/features/cart/CartBadge.tsx:8:29: error react-hooks(rules-of-hooks): React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render.
help: Move the Hook call before the condition, or call it unconditionally and branch inside the Hook/effect instead.
src/features/cart/CartBadge.tsx:11:25: warning react-hooks(exhaustive-deps): React Hook useEffect has a missing dependency: 'cartId'
help: Either include it or remove the dependency array.
src/features/cart/CartBadge.tsx:10:3: error react-hooks(rules-of-hooks): React Hook "useEffect" is called conditionally. React Hooks must be called in the exact same order in every component render.
help: Move the Hook call before the condition, or call it unconditionally and branch inside the Hook/effect instead.
exit code: 1
Hook kuralları error, dependency kuralı warning seviyesinde; bu yüzden komut 1 ile çıkıyor ve CI'da build'i durdurabiliyor. Temiz projede Oxlint hiçbir şey yazmıyor ve 0 ile çıkıyor. React ekibinin kendi lint paketi eslint-plugin-react-hooks (7.1) ESLint kullanan projeler için hâlâ referans; Oxlint aynı kuralların kendi uygulamasını içeriyor. Dependency array'in mantığını altıncı yazıda ayrıntılı göreceğiz.
StrictMode: Geliştirmede Bilerek Çift Çalıştırma
main.tsx'teki <StrictMode>, altındaki ağaç için yalnız geliştirmede çalışan ek kontroller açıyor. "Önce Tahmin Et"teki kart, npm run dev ile açılıp bir kez tıklandığında Playwright'ın topladığı konsol:
render BookCard Dune
render BookCard Dune
effect: subscribe 9780441172719
cleanup: unsubscribe 9780441172719
effect: subscribe 9780441172719
--- click
render BookCard Dune
render BookCard Dune
Aynı kod npm run build ve npm run preview ile:
render BookCard Dune
effect: subscribe 9780441172719
--- click
render BookCard Dune
Geliştirmede her render iki kez, effect ise kurulum, temizlik, tekrar kurulum sırasıyla çalışıyor. Production'da hepsi bir kez. Bu bir hata değil, kasıtlı bir test: react.dev'in StrictMode sayfasına göre React, saf olması gereken fonksiyonları (component gövdesi, state başlangıç fonksiyonları, updater'lar) geliştirmede iki kez çağırıyor ve her effect'i bir kez fazladan kurup temizliyor. Saf bir fonksiyon iki kez çalışınca sonuç değişmez; render içinde bir array'e push yapan ya da global bir sayacı artıran fonksiyon ise ikinci çağrıda hatasını ele veriyor. Effect tarafında da aynı mantık: cleanup'ı eksik bir abonelik, interval ya da bağlantı, mount, unmount, mount döngüsünde iki kez açık kalıyor ve sorunu ilk günden görüyorsun.
Angular tarafında buna en yakın şey geliştirme modundaki ek change detection kontrolü (ExpressionChangedAfterItHasBeenCheckedError): o da yalnız geliştirmede çalışıyor ve aynı soruyu soruyor, "bu hesaplama iki kez yapılınca aynı sonucu veriyor mu?". Fark şurada: React yalnız uyarmıyor, kodu gerçekten iki kez çalıştırıyor. Bu yüzden StrictMode'u kapatmak sorunu çözmez, yalnız görünmez yapar. React DevTools kuruluysa ikinci render'daki console.log satırları soluk renkte görünüyor; DevTools'ta bunları tamamen gizleyen, varsayılanda kapalı bir ayar da var.
React DevTools
React DevTools, Chrome, Firefox ve Edge için tarayıcı eklentisi olarak dağıtılıyor; React kullanan bir sayfada geliştirici araçlarına iki panel ekliyor. Components paneli, DOM ağacı yerine component ağacını gösteriyor: seçtiğin component'in props'unu, state'ini ve hook'larını okuyup değiştirebiliyorsun. Angular DevTools'un component ağacını kullandıysan bu panel tanıdık gelecek. Profiler paneli bir etkileşimi kaydedip hangi component'in kaç kez ve ne kadar sürede render edildiğini ve neden render edildiğini gösteriyor; performans yazısında bunu ölçüm için kullanacağız.
Bu yazının çıktılarını headless bir tarayıcıda topladığımız için eklenti burada çalıştırılmadı; bu bölüm react.dev'in React Developer Tools sayfasına dayanıyor. Eklentiyi kendi tarayıcına kurup npm run dev ile açtığın sayfada Components panelinden App'i seçersen useState değerini görebilir ve oradan değiştirerek kartın etiketinin değiştiğini izleyebilirsin.
API'ye Bağlanmak: Dev Proxy mi, CORS mu?
Bookstore API, README'sine göre geliştirmede http://localhost:5080'de çalışıyor; Vite'ın dev server'ı ise varsayılan olarak 5173'te. Tarayıcı açısından bunlar iki ayrı origin. Uygulama fetch('http://localhost:5080/api/books') çağırırsa tarayıcı CORS (Cross-Origin Resource Sharing; tarayıcının başka origin'e yapılan isteklere hangi şartlarla izin verdiğini belirleyen kurallar) kurallarını uygular ve API'nin Access-Control-Allow-Origin header'ı göndermesini bekler. Bookstore API'de CORS ayarı yok, çünkü Angular istemcisi de bir proxy kullanıyor.
İki seçenek var: API'ye geliştirme origin'ini izin veren bir CORS policy eklemek ya da dev server'ın /api isteklerini API'ye aktarmasını sağlamak. Angular istemcisindeki proxy.conf.json ikinci yolu seçmiş; biz de aynısını Vite ile yapıyoruz. Yukarıdaki vite.config.ts'te /api ile başlayan her istek apiTarget'a gidiyor; tarayıcı yalnız kendi origin'iyle konuşuyor. Hedefi BOOKSTORE_API environment variable'ı ile değiştirilebilir yaptık; bu yazıdaki denemelerde API 127.0.0.1:52141'de, dev server ise 52142'de çalıştı:
BOOKSTORE_API=http://127.0.0.1:52141 npm run dev -- --host 127.0.0.1 --port 52142
Windows'ta PowerShell kullanıyorsan önce $env:BOOKSTORE_API = "http://127.0.0.1:52141" yazıp sonra npm run dev çalıştırman yeterli; API'yi README'deki gibi 5080'de çalıştırıyorsan değişkene hiç gerek yok. Dev server'ın çıktısı:
> bookstore-react@0.0.0 dev
> vite --host 127.0.0.1 --port 52142
VITE v8.3.8 ready in 52 ms
➜ Local: http://127.0.0.1:52142/
Proxy üzerinden kategoriler:
curl -i http://127.0.0.1:52142/api/categories
HTTP/1.1 200 OK
Vary: Origin
connection: close
content-type: application/json; charset=utf-8
server: Kestrel
Transfer-Encoding: chunked
[{"name":"Classics","slug":"classics"},{"name":"Dystopian","slug":"dystopian"},{"name":"Fantasy","slug":"fantasy"},{"name":"Science Fiction","slug":"science-fiction"}]
server: Kestrel header'ı yanıtın ASP.NET Core'dan geldiğini gösteriyor. Farkı tarayıcıda görmek için Playwright ile 52142'deki sayfadan iki istek attık: biri proxy'ye, biri doğrudan API'ye.
fetch /api/categories (proxy): 200
console error: Access to fetch at 'http://127.0.0.1:52141/api/categories' from origin 'http://127.0.0.1:52142' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
console error: Failed to load resource: net::ERR_FAILED
fetch http://127.0.0.1:52141/api/categories (direct): TypeError: Failed to fetch
Doğrudan istek API'ye ulaştı ve API 200 döndü (curl ile aynı adresi çağırdığımızda yanıt geliyor), ama tarayıcı yanıtı JavaScript'e vermedi. CORS bir sunucu güvenlik duvarı değil, tarayıcının uyguladığı bir okuma kuralı. Proxy kullanınca bu kural devreye girmiyor, çünkü tarayıcının gözünde istek aynı origin'e gidiyor. Veri çekmeyi düzgün yapmak, yükleme ve hata durumlarını yönetmek altıncı ve sekizinci yazıların konusu; şimdilik bağlantının kurulduğunu biliyoruz.
Üretimde Dikkat
Vite'ın proxy'si yalnız npm run dev sırasında var. npm run build statik dosyalar üretiyor; production'da /api isteklerinin API'ye ulaşmasını ya aynı origin'de çalışan bir reverse proxy (IIS, nginx ya da YARP (Microsoft'un .NET için reverse proxy kütüphanesi); sitedeki API Gateway ve YARP serisi bu konuyu anlatıyor) ya da API'deki bir CORS policy sağlamalı. Uygulama kodunda göreli /api/... adresleri kullanmak, iki ortamda da aynı kodun çalışmasını sağlıyor.
BOOKSTORE_API gibi değişkenler yalnız vite.config.ts'te, yani Node.js tarafında okunuyor; tarayıcıya giden koda girmiyor. Tarayıcı koduna değer aktarmak için Vite'ın import.meta.env mekanizması var ve oraya giren her değer build çıktısında herkesin okuyabileceği bir string oluyor; bunu ve secret'ları neden oraya koymamak gerektiğini uçtan uca uygulama yazısında ele alacağız.
StrictMode'u production'da kapatmana gerek yok; kontrolleri zaten yalnız geliştirmede çalışıyor. Lint'i ise CI'da çalıştır: npm run lint hook kuralı ihlalinde 1 ile çıktığı için pipeline'ı durdurmaya yetiyor.
Sık Düşülen Tuzaklar
StrictMode'daki çift çalışmayı hata sanıp <StrictMode>'u silmek en yaygın tepki. Çift log ya da iki kez giden istek, kodda cleanup eksikliğine ya da saf olmayan bir render'a işaret ediyor; düzeltilecek yer orası.
Path alias'ı yalnız bir tarafta tanımlamak ikinci tuzak. paths'i tsconfig'e yazıp Vite'a söylemezsen tsc geçer ama dev server "Failed to resolve import" hatası verir; tersinde editör ve tsc kırmızı çizer. Vite 8'de resolve.tsconfigPaths: true bu ikiliği tek kaynağa indiriyor.
Üçüncü tuzak, kodun içine API'nin tam adresini yazmak: fetch('http://localhost:5080/api/books'). Bu satır proxy'yi devre dışı bırakıyor, tarayıcıyı CORS kuralına geri sokuyor ve kodu tek bir geliştirme ortamına bağlıyor. Uygulama kodunda göreli /api/... adresi kalsın; adresin nereye gideceğine geliştirmede Vite, production'da reverse proxy karar versin.
Son tuzak, kodu teknik türe göre klasörlemek: components/, hooks/, utils/. Küçük projede masum görünüyor; yirmi ekranlık bir uygulamada sepetle ilgili bir değişiklik dört klasörde dosya açmayı gerektiriyor. Feature klasörleri ve sınırların nasıl korunacağı, UI Mimari Pattern'leri serisindeki "Büyük Angular Uygulamasında Sınırlar: Feature Folder, Facade ve Katmanlar" yazısında Angular için anlatıldı; aynı fikir React'te de geçerli.
Küçük Bir Deneme
★☆☆ features/cart/CartSummary.tsx'i @/ yerine göreli yolla import et, sonra CartSummary'i features/shared/ altına taşı. Hangi import'ları düzeltmen gerekti?
★★☆ "Önce Tahmin Et"teki effect'ten cleanup fonksiyonunu kaldır ve npm run dev ile sayfayı aç. Konsolda ne değişti? Bu effect gerçek bir WebSocket aboneliği açıyor olsaydı production'da ne olurdu, geliştirmede ne olurdu?
★★★ Bookstore API'ye geliştirme origin'ine izin veren bir CORS policy ekle (ASP.NET Core Web API serisindeki CORS anlatımına bakabilirsin), Vite proxy'sini kapat ve uygulamada fetch'i tam adresle çağır. Tarayıcının network sekmesinde kaç istek görüyorsun? OPTIONS isteği hangi durumda gidiyor?
Proje iskeleti hazır. Sıradaki yazıda JSX'in neye derlendiğini, props'u, children ile composition'ı ve liste render'ını, key'in neden array index'i olmaması gerektiğini gerçek bir hatayla göreceğiz.