İçeriğe geç
Sedat Demir
Geri dön

React Router v7 Framework Modu: Remix ile Birleşme ve Full-Stack Geçiş Rehberi

React Router v7 Framework Modu: Remix ile Birleşme ve Full-Stack Geçiş Rehberi

React ekosisteminde 2024'ün en önemli gelişmelerinden biri sessiz sedasız gerçekleşti: Remix, React Router ile birleşti. React Router v7 artık sadece client-side bir yönlendirme kütüphanesi değil; server-side rendering, data loading, form handling ve daha fazlasını sunan tam kapsamlı bir full-stack framework. Bu yazıda bu birleşmenin ne anlama geldiğini, nasıl çalıştığını ve projelerinizde nasıl kullanabileceğinizi adım adım inceleyeceğiz.


Neden Birleştiler? Arka Plan

Remix ekibi, 2022'den bu yana React Router ile Remix arasındaki çizgiyi aşamalı olarak bulanıklaştırıyordu. Remix v2 zaten React Router v6'nın üzerine inşa edilmişti ve iki proje arasındaki fark giderek azalıyordu. Mantıksal sonuç belliydi: İki ayrı proje yerine tek bir çatı altında birleşmek.

Bu birleşmenin temel motivasyonları:

Ryan Florence'ın ifadesiyle: "Remix, React Router'ın bir sonraki büyük versiyonudur."


İki Çalışma Modu: Library vs Framework

React Router v7'nin en önemli özelliği iki farklı modda çalışabilmesidir:

1. Library Modu (Klasik Kullanım)

Daha önceki React Router sürümlerinde alışık olduğunuz kullanım. Sadece client-side routing yaparsınız, herhangi bir sunucu tarafı işlevsellik yoktur. Mevcut SPA projelerinizde doğrudan kullanabilirsiniz.

// Library modu - klasik kullanım
import { BrowserRouter, Routes, Route } from "react-router";

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/products/:id" element={<Product />} />
      </Routes>
    </BrowserRouter>
  );
}

2. Framework Modu (Remix Gücü)

İşte asıl heyecan verici kısım burası. Framework modunu etkinleştirdiğinizde React Router, Remix'in sunduğu tüm yetenekleri devralır:


Framework Moduna Başlarken

Framework modunu kullanmak için projenizi Vite üzerinden yapılandırmanız gerekir. React Router v7, build aracı olarak Vite kullanır.

Kurulum

npx create-react-router@latest my-app
cd my-app
npm install
npm run dev

Vite Konfigürasyonu

// vite.config.ts
import { reactRouter } from "@react-router/dev/vite";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";

export default defineConfig({
  plugins: [
    reactRouter(), // Framework modunu etkinleştirir
    tsconfigPaths(),
  ],
});

React Router Konfigürasyonu

// react-router.config.ts
import type { Config } from "@react-router/dev/config";

export default {
  // SSR'ı etkinleştir
  ssr: true,

  // Uygulamanın dizin yapısı
  appDirectory: "app",
} satisfies Config;

Dosya Tabanlı Yönlendirme (File-Based Routing)

Framework modunda route'larınızı routes.ts dosyasında tanımlarsınız. Bu, Remix'in dosya tabanlı yönlendirme sisteminin evrimleşmiş halidir:

// app/routes.ts
import {
  type RouteConfig,
  index,
  route,
  layout,
  prefix,
} from "@react-router/dev/routes";

export default [
  // Ana sayfa
  index("routes/home.tsx"),

  // Basit route
  route("about", "routes/about.tsx"),

  // Layout ile sarmalanmış route'lar
  layout("routes/dashboard/layout.tsx", [
    index("routes/dashboard/index.tsx"),
    route("settings", "routes/dashboard/settings.tsx"),
    route("profile", "routes/dashboard/profile.tsx"),
  ]),

  // Prefix ile gruplandırma
  ...prefix("api", [
    route("users", "routes/api/users.tsx"),
    route("products", "routes/api/products.tsx"),
  ]),
] satisfies RouteConfig;

Bu yapı, Remix'in v2_routeConvention'ından daha esnek ve açıktır. Route'larınızı istediğiniz klasör yapısında organize edebilir, routes.ts içinde sadece ilişkilendirmeyi yaparsınız.


Loader ve Action: Sunucu Tarafı Veri Yönetimi

Framework modunun en güçlü özelliği, her route için sunucu tarafında çalışan loader ve action fonksiyonlarıdır.

Loader ile Veri Yükleme

// app/routes/products.tsx
import type { Route } from "./+types/products";

// Bu fonksiyon SUNUCUDA çalışır
export async function loader({ request }: Route.LoaderArgs) {
  const url = new URL(request.url);
  const search = url.searchParams.get("q") || "";

  const products = await db.product.findMany({
    where: {
      name: { contains: search, mode: "insensitive" },
    },
    orderBy: { createdAt: "desc" },
  });

  return { products, search };
}

// Component otomatik olarak loader verisini alır
export default function Products({ loaderData }: Route.ComponentProps) {
  const { products, search } = loaderData;

  return (
    <div>
      <h1>Ürünler</h1>
      <SearchBar defaultValue={search} />
      <ul>
        {products.map((product) => (
          <li key={product.id}>
            <a href={`/products/${product.id}`}>{product.name}</a>
            <span>{product.price} TL</span>
          </li>
        ))}
      </ul>
    </div>
  );
}

Action ile Form İşleme

// app/routes/products.new.tsx
import { redirect } from "react-router";
import type { Route } from "./+types/products.new";

// Form submit edildiğinde SUNUCUDA çalışır
export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  const name = formData.get("name") as string;
  const price = parseFloat(formData.get("price") as string);

  // Validasyon
  const errors: Record<string, string> = {};
  if (!name || name.length < 2) {
    errors.name = "Ürün adı en az 2 karakter olmalıdır.";
  }
  if (isNaN(price) || price <= 0) {
    errors.price = "Geçerli bir fiyat giriniz.";
  }

  if (Object.keys(errors).length > 0) {
    return { errors };
  }

  const product = await db.product.create({
    data: { name, price },
  });

  return redirect(`/products/${product.id}`);
}

export default function NewProduct({ actionData }: Route.ComponentProps) {
  const errors = actionData?.errors;

  return (
    <div>
      <h1>Yeni Ürün Ekle</h1>
      <form method="post">
        <div>
          <label htmlFor="name">Ürün Adı</label>
          <input type="text" id="name" name="name" required />
          {errors?.name && <p className="error">{errors.name}</p>}
        </div>
        <div>
          <label htmlFor="price">Fiyat (TL)</label>
          <input type="number" id="price" name="price" step="0.01" required />
          {errors?.price && <p className="error">{errors.price}</p>}
        </div>
        <button type="submit">Kaydet</button>
      </form>
    </div>
  );
}

Dikkat edin: useLoaderData() ve useActionData() hook'ları yerine artık props üzerinden type-safe veri alıyorsunuz. Bu, TypeScript deneyimini büyük ölçüde iyileştiriyor.


Type-Safe Routing: Otomatik Tip Üretimi

React Router v7'nin en çarpıcı yeniliklerinden biri otomatik tip üretimidir. Her route dosyası için ./+types/ dizininde tipler otomatik oluşturulur:

// app/routes/products.$id.tsx
import type { Route } from "./+types/products.$id";

export async function loader({ params }: Route.LoaderArgs) {
  // params.id otomatik olarak string tipinde!
  const product = await db.product.findUnique({
    where: { id: params.id },
  });

  if (!product) {
    throw new Response("Bulunamadı", { status: 404 });
  }

  return { product };
}

export default function ProductDetail({ loaderData }: Route.ComponentProps) {
  // loaderData.product tipi otomatik olarak çıkarılır
  const { product } = loaderData;

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.price} TL</p>
    </div>
  );
}

Artık useParams() ile as string yazmak veya useLoaderData<typeof loader>() gibi manuel tip tanımlamaları yapmak zorunda değilsiniz.


Pending UI ve Optimistic Updates

React Router v7 framework modu, navigasyon sırasında kullanıcıya geri bildirim vermek için güçlü araçlar sunar:

import { useNavigation, Form } from "react-router";

export default function ProductForm() {
  const navigation = useNavigation();
  const isSubmitting = navigation.state === "submitting";

  return (
    <Form method="post">
      <input type="text" name="name" disabled={isSubmitting} />
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "Kaydediliyor..." : "Kaydet"}
      </button>
    </Form>
  );
}

Mevcut Projeden Geçiş Stratejileri

Remix v2'den React Router v7'ye

Bu geçiş nispeten basittir çünkü API'ler büyük ölçüde aynıdır:

  1. Paketleri değiştirin:
npm uninstall @remix-run/react @remix-run/node @remix-run/serve
npm install react-router @react-router/node @react-router/serve
  1. Import'ları güncelleyin:
// Eski (Remix v2)
import { useLoaderData, json } from "@remix-run/react";

// Yeni (React Router v7)
import { useLoaderData } from "react-router";
// json() artık gerekli değil, doğrudan obje dönebilirsiniz
  1. json() wrapper'ını kaldırın:
// Eski
export async function loader() {
  return json({ message: "hello" });
}

// Yeni - doğrudan return
export async function loader() {
  return { message: "hello" };
}
  1. Route config dosyasını oluşturun (routes.ts)

  2. vite.config.ts'i güncelleyin (@react-router/dev/vite kullanın)

React Router v6 SPA'dan Framework Moduna

Bu geçiş daha kapsamlıdır ve kademeli yaklaşım önerilir:

  1. Önce v7 library moduna güncelleyin.
  2. Vite plugin'ini ekleyin.
  3. Route'larınızı tek tek framework moduna taşıyın.
  4. useEffect + fetch kalıplarını loader'lara dönüştürün.
// ÖNCE: Client-side veri çekme
function Products() {
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch("/api/products")
      .then((res) => res.json())
      .then(setProducts)
      .finally(() => setLoading(false));
  }, []);

  if (loading) return <Spinner />;
  return <ProductList products={products} />;
}

// SONRA: Server-side loader
export async function loader() {
  const products = await db.product.findMany();
  return { products };
}

export default function Products({ loaderData }: Route.ComponentProps) {
  return <ProductList products={loaderData.products} />;
}

SSR vs SPA vs Static: Esnek Dağıtım

React Router v7 framework modu, farklı dağıtım stratejilerini destekler:

// react-router.config.ts
import type { Config } from "@react-router/dev/config";

export default {
  // Tam SSR (varsayılan)
  ssr: true,

  // Veya SPA modu (SSR kapalı)
  // ssr: false,

  // Veya belirli route'ları pre-render et (SSG)
  async prerender() {
    return ["/", "/about", "/pricing"];
  },
} satisfies Config;

Bu esneklik sayesinde aynı proje içinde bazı sayfalar SSR ile, bazıları statik olarak oluşturulabilir.


Deployment Seçenekleri

React Router v7 framework modu çeşitli platformlarda çalışır:

// server.ts - Node.js adapter örneği
import { createRequestHandler } from "@react-router/node";
import express from "express";

const app = express();

app.use(
  express.static("build/client", {
    maxAge: "1y",
    immutable: true,
  })
);

app.all("*", createRequestHandler({ build: () => import("./build/server") }));

app.listen(3000, () => {
  console.log("Server 3000 portunda çalışıyor");
});

Performans Avantajları

Framework modunun SPA yaklaşımına göre somut avantajları:

Özellik SPA (Library Modu) Framework Modu
İlk Yükleme Yavaş (waterfall) Hızlı (SSR + streaming)
SEO Zayıf Güçlü
Code Splitting Manuel Otomatik
Data Fetching Client waterfall Paralel server-side
Tip Güvenliği Manuel Otomatik
Progressive Enhancement Yok Var

Sonuç

React Router v7'nin framework modu, React ekosisteminde önemli bir dönüm noktasıdır. Remix'in yıllarca geliştirdiği full-stack yetenekler artık dünyanın en popüler React routing kütüphanesinin bir parçası oldu.

Özetle:

Bu birleşme, "hangi router'ı kullanmalıyım?" sorusunu ortadan kaldırıyor. React Router v7 artık hem basit SPA'lar hem de karmaşık full-stack uygulamalar için tek bir çözüm sunuyor. Eğer hâlâ useEffect + fetch ile veri çekiyorsanız, framework moduna geçerek hem performansınızı hem de kod kalitenizi bir üst seviyeye taşımanın tam zamanı.


Share this post on:

Önceki Yazı
React Foundation: Linux Foundation Altında Yeni Bir Dönem Başlıyor
Sonraki Yazı
React 19 use() Hook: Promise ve Context'i Render Sırasında Okuma Rehberi