React Server Components ile Modern Web Uygulamaları

Yunus Emre Güzel
10 Ocak 202520 dkReact
React Server Components ile Modern Web Uygulamaları

React Server Components ile Modern Web Uygulamaları

React Server Components (RSC), React ekosisteminde devrim niteliğinde bir yenilik olarak karşımıza çıkıyor. Bu teknoloji, sunucu tarafı rendering ve client tarafı interaktiviteyi harmanlayarak, modern web uygulamalarının performans ve geliştirme deneyimini yeniden şekillendiriyor. Bu yazıda, RSC'nin derinlemesine incelemesini yapacak ve gerçek dünya uygulamalarında nasıl kullanılabileceğini öğreneceğiz.

React Server Components Nedir?

React Server Components, React uygulamalarının bir kısmını sunucu tarafında render etmeyi sağlayan yeni bir paradigmadır. Bu yaklaşım, geleneksel client-side rendering ile server-side rendering'in en iyi yanlarını birleştirir. RSC, React 18 ile birlikte gelen ve Next.js 13+ sürümlerinde varsayılan olarak desteklenen bir özelliktir.

Temel Kavramlar

  1. Zero-Bundle-Size Components: Server Components client bundle'a dahil edilmez
  2. Automatic Code Splitting: İstemci tarafı kod otomatik olarak bölünür
  3. Streaming Server Rendering: Sayfa parça parça render edilebilir
  4. Server-Side Data Access: Veritabanına doğrudan erişim

Temel Özellikler ve Implementasyon

// app/components/ServerComponent.tsx
// Bu bir server component'tir - '.server.tsx' uzantısı ile de belirtilebilir
async function ProductList() {
  // Sunucu tarafında direkt veritabanına erişebilirsiniz
  const products = await db.products.findMany({
    include: {
      category: true,
      reviews: {
        take: 3,
        orderBy: {
          createdAt: 'desc'
        }
      }
    }
  });

  return (
    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
      {products.map(product => (
        <ProductCard 
          key={product.id} 
          product={product}
          reviews={product.reviews}
          category={product.category}
        />
      ))}
    </div>
  );
}

// app/components/ProductCard.tsx
// Statik UI için server component
function ProductCard({ product, reviews, category }) {
  return (
    <div className="border rounded-lg p-4 shadow-sm">
      <div className="aspect-w-16 aspect-h-9 mb-4">
        <Image
          src={product.imageUrl}
          alt={product.name}
          fill
          className="object-cover rounded-md"
        />
      </div>
      <h3 className="text-lg font-semibold">{product.name}</h3>
      <p className="text-sm text-gray-600">{category.name}</p>
      <div className="mt-2">
        <ProductPrice price={product.price} discount={product.discount} />
      </div>
      <div className="mt-4">
        <ProductReviews reviews={reviews} />
      </div>
      <AddToCartButton client productId={product.id} /> {/* Client Component */}
    </div>
  );
}

// app/components/AddToCartButton.client.tsx
// İnteraktif özellikler için client component
'use client';

import { useState } from 'react';
import { useCart } from '~/hooks/useCart';

interface AddToCartButtonProps {
  productId: string;
}

function AddToCartButton({ productId }: AddToCartButtonProps) {
  const [isLoading, setIsLoading] = useState(false);
  const { addToCart } = useCart();

  const handleClick = async () => {
    try {
      setIsLoading(true);
      await addToCart(productId);
      toast.success('Ürün sepete eklendi');
    } catch (error) {
      toast.error('Bir hata oluştu');
      console.error('Add to cart error:', error);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <button
      onClick={handleClick}
      disabled={isLoading}
      className={`
        w-full mt-4 px-4 py-2 rounded-lg
        bg-blue-600 hover:bg-blue-700
        text-white font-medium
        transition duration-200
        disabled:opacity-50 disabled:cursor-not-allowed
      `}
    >
      {isLoading ? (
        <span className="flex items-center justify-center">
          <Spinner className="w-4 h-4 mr-2" />
          Ekleniyor...
        </span>
      ) : (
        'Sepete Ekle'
      )}
    </button>
  );
}

Server Components'in Avantajları ve Kullanım Senaryoları

1. Performans İyileştirmeleri ve Bundle Size Optimizasyonu

Server Components, bundle size'ı önemli ölçüde azaltır çünkü sunucu tarafında çalışan kodlar client bundle'a dahil edilmez. Bu özellikle büyük dependencies kullanan komponentler için çok önemlidir.

// Geleneksel yaklaşım - Tüm dependencies client'a gönderilir
import { marked } from 'marked'; // 1MB
import { prism } from 'prismjs'; // 2MB
import { chartjs } from 'chart.js'; // 800KB
import { pdfLib } from '@pdf-lib/full'; // 2.5MB

// Server Component yaklaşımı - Dependencies sunucuda kalır
async function ReportGenerator({ data, template }) {
  // Ağır işlemler sunucuda yapılır
  const markdown = await marked(template);
  const highlighted = prism.highlight(data.code, 'javascript');
  const chart = await generateChart(data.metrics);
  const pdf = await generatePDF(data.content);
  
  return (
    <div className="report-container">
      <div dangerouslySetInnerHTML={{ __html: markdown }} />
      <div className="code-block">{highlighted}</div>
      <div className="chart-container">
        <Image src={chart} alt="Metrics Chart" />
      </div>
      <a href={pdf} download>
        PDF İndir
      </a>
    </div>
  );
}

// Yardımcı fonksiyonlar
async function generateChart(metrics) {
  const chart = new Chart(/* ... */);
  return chart.toBase64Image();
}

async function generatePDF(content) {
  const pdfDoc = await PDFDocument.create();
  // ... PDF generation logic
  return pdfDoc.save();
}

2. Veri Erişimi, Güvenlik ve Database Operasyonları

Server Components, veritabanına doğrudan erişim sağlar ve hassas bilgileri client'a göndermeden işleyebilir. Bu özellik, güvenlik ve performans açısından büyük avantajlar sağlar.

// app/components/AdminDashboard.server.tsx
async function AdminDashboard() {
  // Kompleks database sorguları ve joins
  const dashboardData = await prisma.$transaction([
    prisma.user.count(),
    prisma.order.findMany({
      where: {
        status: 'PENDING',
        createdAt: {
          gte: subDays(new Date(), 7)
        }
      },
      include: {
        items: true,
        customer: {
          select: {
            name: true,
            email: true,
            membership: true
          }
        }
      }
    }),
    prisma.revenue.aggregate({
      _sum: {
        amount: true
      },
      where: {
        createdAt: {
          gte: startOfMonth(new Date())
        }
      }
    })
  ]);

  // Hassas verileri filtrele ve dönüştür
  const [userCount, pendingOrders, revenue] = dashboardData;
  const safeOrderData = pendingOrders.map(order => ({
    id: order.id,
    customerName: order.customer.name,
    total: order.items.reduce((sum, item) => sum + item.price, 0),
    status: order.status,
    date: order.createdAt
  }));

  return (
    <div className="admin-dashboard">
      <DashboardStats
        userCount={userCount}
        orderCount={pendingOrders.length}
        revenue={revenue._sum.amount}
      />
      <PendingOrdersList orders={safeOrderData} />
      <RevenueChart data={safeOrderData} client />
    </div>
  );
}

// app/components/RevenueChart.client.tsx
'use client';

import { Line } from 'react-chartjs-2';

function RevenueChart({ data }) {
  const chartData = useMemo(() => ({
    labels: data.map(order => format(order.date, 'MMM dd')),
    datasets: [{
      label: 'Günlük Gelir',
      data: data.map(order => order.total),
      borderColor: 'rgb(75, 192, 192)',
      tension: 0.1
    }]
  }), [data]);

  return (
    <div className="p-4 bg-white rounded-lg shadow">
      <h3 className="text-lg font-semibold mb-4">Gelir Grafiği</h3>
      <Line data={chartData} options={chartOptions} />
    </div>
  );
}

3. SEO ve İlk Yükleme Performansı Optimizasyonu

Server Components, içeriği sunucu tarafında render ettiği için SEO açısından önemli avantajlar sağlar ve ilk yükleme performansını optimize eder.

// app/pages/blog/[slug]/page.tsx
import { Metadata } from 'next';

interface BlogPostParams {
  params: {
    slug: string;
  };
}

// Dinamik metadata generation
export async function generateMetadata(
  { params }: BlogPostParams
): Promise<Metadata> {
  const post = await getBlogPost(params.slug);
  
  return {
    title: `${post.title} | Blog`,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage],
      type: 'article',
      article: {
        publishedTime: post.publishedAt,
        authors: [post.author.name],
        tags: post.tags
      }
    },
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage]
    }
  };
}

// Statik sayfa üretimi için paths
export async function generateStaticParams() {
  const posts = await getAllBlogPosts();
  
  return posts.map((post) => ({
    slug: post.slug,
  }));
}

// Blog post sayfası
async function BlogPost({ params }: BlogPostParams) {
  const post = await getBlogPost(params.slug);
  const relatedPosts = await getRelatedPosts(post.tags);
  
  return (
    <article className="max-w-4xl mx-auto px-4 py-12">
      <BlogHeader
        title={post.title}
        author={post.author}
        date={post.publishedAt}
        readingTime={post.readingTime}
        tags={post.tags}
      />
      
      <div className="prose prose-lg dark:prose-invert mt-8">
        <MDXContent content={post.content} />
      </div>
      
      <div className="mt-12">
        <RelatedPosts posts={relatedPosts} />
      </div>
      
      <div className="mt-8">
        <CommentSection client postId={post.id} />
      </div>
    </article>
  );
}

Server Components ile Modern Uygulama Mimarisi

1. Hybrid Rendering ve Kompozisyon Stratejileri

Server ve Client Components'i etkili bir şekilde birleştirmek için stratejik yaklaşımlar ve kompozisyon patterns:

// app/layout/AppLayout.server.tsx
async function AppLayout({ children }) {
  const user = await getCurrentUser();
  const notifications = await getUnreadNotifications(user?.id);
  const preferences = await getUserPreferences(user?.id);
  
  return (
    <div className="min-h-screen bg-gray-50 dark:bg-gray-900">
      <Header>
        <Navigation items={navigationItems} /> {/* Server Component */}
        <SearchBar client /> {/* Client Component */}
        <UserMenu 
          client 
          user={user} 
          notifications={notifications}
        /> {/* Client Component */}
      </Header>
      
      <Sidebar>
        <NavigationMenu items={navigationItems} /> {/* Server Component */}
        <ThemeToggle client /> {/* Client Component */}
      </Sidebar>
      
      <main className="flex-1">
        <Suspense fallback={<PageSkeleton />}>
          {children}
        </Suspense>
      </main>
      
      <Footer /> {/* Server Component */}
    </div>
  );
}

// app/components/UserMenu.client.tsx
'use client';

function UserMenu({ user, notifications }) {
  const [isOpen, setIsOpen] = useState(false);
  const menuRef = useRef(null);
  
  useClickOutside(menuRef, () => setIsOpen(false));
  
  return (
    <div ref={menuRef} className="relative">
      <button
        onClick={() => setIsOpen(!isOpen)}
        className="flex items-center space-x-2"
      >
        <Avatar user={user} />
        {notifications.length > 0 && (
          <span className="absolute -top-1 -right-1 w-4 h-4 bg-red-500 rounded-full text-xs text-white flex items-center justify-center">
            {notifications.length}
          </span>
        )}
      </button>
      
      <AnimatePresence>
        {isOpen && (
          <motion.div
            initial={{ opacity: 0, y: 10 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, y: 10 }}
            className="absolute right-0 mt-2 w-64 bg-white dark:bg-gray-800 rounded-lg shadow-lg"
          >
            <UserMenuContent user={user} notifications={notifications} />
          </motion.div>
        )}
      </AnimatePresence>
    </div>
  );
}

2. Veri Akışı, State Management ve Server Actions

Modern React uygulamalarında veri akışı ve state yönetimi stratejileri:

// app/lib/actions.ts
'use server';

import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';

export async function updateUserProfile(
  userId: string,
  data: UpdateUserProfileData
) {
  try {
    // Form validation
    const validatedData = await updateUserProfileSchema.validate(data);
    
    // Database update
    await prisma.user.update({
      where: { id: userId },
      data: validatedData
    });
    
    // Cache invalidation
    revalidatePath('/profile');
    revalidatePath('/dashboard');
    
    return { success: true };
  } catch (error) {
    return {
      success: false,
      error: error instanceof Error ? error.message : 'An error occurred'
    };
  }
}

// app/components/ProfileForm.client.tsx
'use client';

function ProfileForm({ user }) {
  const [isPending, startTransition] = useTransition();
  const [error, setError] = useState<string | null>(null);
  
  const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    setError(null);
    
    const formData = new FormData(event.currentTarget);
    
    startTransition(async () => {
      const result = await updateUserProfile(user.id, {
        name: formData.get('name'),
        email: formData.get('email'),
        bio: formData.get('bio')
      });
      
      if (!result.success) {
        setError(result.error);
      }
    });
  };
  
  return (
    <form onSubmit={handleSubmit} className="space-y-6">
      {error && (
        <div className="p-4 bg-red-50 text-red-700 rounded-lg">
          {error}
        </div>
      )}
      
      <fieldset disabled={isPending}>
        <div className="space-y-4">
          <FormField
            label="İsim"
            name="name"
            defaultValue={user.name}
          />
          <FormField
            label="E-posta"
            name="email"
            type="email"
            defaultValue={user.email}
          />
          <FormField
            label="Hakkımda"
            name="bio"
            as="textarea"
            defaultValue={user.bio}
          />
        </div>
        
        <button
          type="submit"
          className={`
            mt-6 w-full px-4 py-2 rounded-lg
            bg-blue-600 hover:bg-blue-700
            text-white font-medium
            transition duration-200
            disabled:opacity-50
          `}
        >
          {isPending ? 'Kaydediliyor...' : 'Kaydet'}
        </button>
      </fieldset>
    </form>
  );
}

İleri Seviye Optimizasyon Teknikleri

1. Streaming ve Suspense ile Progressive Rendering

// app/pages/dashboard/page.tsx
export default async function DashboardPage() {
  return (
    <div className="dashboard-container">
      {/* Kritik UI hemen yüklenir */}
      <header className="dashboard-header">
        <h1>Dashboard</h1>
        <QuickActions client />
      </header>
      
      <div className="dashboard-grid">
        {/* Ana metrikler öncelikli olarak yüklenir */}
        <Suspense fallback={<MetricsSkeleton />}>
          <DashboardMetrics />
        </Suspense>
        
        {/* Grafik verisi daha sonra stream edilir */}
        <Suspense fallback={<ChartSkeleton />}>
          <RevenueChart />
        </Suspense>
        
        {/* Tablo verisi en son yüklenir */}
        <Suspense fallback={<TableSkeleton />}>
          <RecentTransactions />
        </Suspense>
      </div>
    </div>
  );
}

// app/components/DashboardMetrics.tsx
async function DashboardMetrics() {
  // Parallel data fetching
  const [revenue, orders, users] = await Promise.all([
    getRevenueMetrics(),
    getOrderMetrics(),
    getUserMetrics()
  ]);
  
  return (
    <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
      <MetricCard
        title="Toplam Gelir"
        value={formatCurrency(revenue.total)}
        trend={revenue.trend}
        icon={<CurrencyIcon />}
      />
      <MetricCard
        title="Siparişler"
        value={orders.count}
        trend={orders.trend}
        icon={<ShoppingCartIcon />}
      />
      <MetricCard
        title="Aktif Kullanıcılar"
        value={users.active}
        trend={users.trend}
        icon={<UsersIcon />}
      />
    </div>
  );
}

2. Error Handling ve Recovery Stratejileri

// app/components/ErrorBoundary.client.tsx
'use client';

function ErrorBoundary({
  children,
  fallback,
  onError,
}: {
  children: React.ReactNode;
  fallback: React.ReactNode;
  onError?: (error: Error) => void;
}) {
  const [error, setError] = useState<Error | null>(null);
  
  useEffect(() => {
    if (error && onError) {
      onError(error);
    }
  }, [error, onError]);
  
  if (error) {
    return (
      <div className="error-container">
        {typeof fallback === 'function' ? fallback(error) : fallback}
        <button
          onClick={() => setError(null)}
          className="mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg"
        >
          Yeniden Dene
        </button>
      </div>
    );
  }
  
  return (
    <ErrorBoundaryComponent
      onError={(error) => setError(error)}
    >
      {children}
    </ErrorBoundaryComponent>
  );
}

// Kullanım örneği
<ErrorBoundary
  fallback={error => (
    <div className="p-4 bg-red-50 rounded-lg">
      <h3 className="text-red-800 font-medium">
        Bir hata oluştu
      </h3>
      <p className="text-red-600 mt-1">
        {error.message}
      </p>
    </div>
  )}
  onError={(error) => {
    // Error reporting service'e gönder
    reportError(error);
  }}
>
  <ComplexDataVisualization data={data} />
</ErrorBoundary>

Performans Monitoring ve Optimizasyon

1. Core Web Vitals ve Real User Monitoring

// app/components/PerformanceMonitor.client.tsx
'use client';

function PerformanceMonitor() {
  useEffect(() => {
    if (typeof window !== 'undefined') {
      // Core Web Vitals monitoring
      const vitalsObserver = new PerformanceObserver((list) => {
        list.getEntries().forEach((entry) => {
          const metric = {
            name: entry.name,
            value: entry.value,
            rating: getRating(entry.name, entry.value),
            timestamp: new Date().toISOString()
          };
          
          // Metric'i analytics service'e gönder
          reportWebVital(metric);
        });
      });
      
      vitalsObserver.observe({
        entryTypes: ['largest-contentful-paint', 'fid', 'cls', 'ttfb']
      });
      
      // Resource timing monitoring
      const resourceObserver = new PerformanceObserver((list) => {
        list.getEntries().forEach((entry) => {
          if (entry.initiatorType === 'fetch' || entry.initiatorType === 'xmlhttprequest') {
            const timing = {
              name: entry.name,
              duration: entry.duration,
              startTime: entry.startTime,
              initiatorType: entry.initiatorType
            };
            
            // API timing metriklerini kaydet
            reportApiTiming(timing);
          }
        });
      });
      
      resourceObserver.observe({ entryTypes: ['resource'] });
      
      // Memory usage monitoring
      if ('memory' in performance) {
        setInterval(() => {
          const memory = {
            jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
            totalJSHeapSize: performance.memory.totalJSHeapSize,
            usedJSHeapSize: performance.memory.usedJSHeapSize
          };
          
          reportMemoryUsage(memory);
        }, 60000); // Her dakika
      }
      
      return () => {
        vitalsObserver.disconnect();
        resourceObserver.disconnect();
      };
    }
  }, []);

  return null;
}

// Yardımcı fonksiyonlar
function getRating(metric: string, value: number): 'good' | 'needs-improvement' | 'poor' {
  switch (metric) {
    case 'LCP':
      return value <= 2500 ? 'good' : value <= 4000 ? 'needs-improvement' : 'poor';
    case 'FID':
      return value <= 100 ? 'good' : value <= 300 ? 'needs-improvement' : 'poor';
    case 'CLS':
      return value <= 0.1 ? 'good' : value <= 0.25 ? 'needs-improvement' : 'poor';
    default:
      return 'good';
  }
}

2. Development Tools ve Debug Utilities

// app/utils/debug.ts
export function createDebugger(namespace: string) {
  return {
    log: (...args: any[]) => {
      if (process.env.NODE_ENV === 'development') {
        console.log(`[${namespace}]`, ...args);
      }
    },
    
    warn: (...args: any[]) => {
      if (process.env.NODE_ENV === 'development') {
        console.warn(`[${namespace}]`, ...args);
      }
    },
    
    error: (...args: any[]) => {
      if (process.env.NODE_ENV === 'development') {
        console.error(`[${namespace}]`, ...args);
      }
    },
    
    group: (label: string) => {
      if (process.env.NODE_ENV === 'development') {
        console.group(`[${namespace}] ${label}`);
      }
    },
    
    groupEnd: () => {
      if (process.env.NODE_ENV === 'development') {
        console.groupEnd();
      }
    },
    
    time: (label: string) => {
      if (process.env.NODE_ENV === 'development') {
        console.time(`[${namespace}] ${label}`);
      }
    },
    
    timeEnd: (label: string) => {
      if (process.env.NODE_ENV === 'development') {
        console.timeEnd(`[${namespace}] ${label}`);
      }
    }
  };
}

// Kullanım örneği
const debug = createDebugger('ServerComponents');

async function ComplexOperation() {
  debug.time('operation');
  debug.group('Complex Operation');
  
  try {
    debug.log('Starting operation...');
    // ... işlemler
    debug.log('Operation completed');
  } catch (error) {
    debug.error('Operation failed:', error);
  } finally {
    debug.groupEnd();
    debug.timeEnd('operation');
  }
}

Sonuç

React Server Components, modern web uygulamaları geliştirme yaklaşımımızı önemli ölçüde değiştiriyor. Bu teknoloji:

  • Bundle size optimizasyonu
  • Daha iyi SEO
  • Gelişmiş performans
  • Güvenli veri erişimi
  • Daha iyi geliştirici deneyimi
  • Progressive enhancement
  • Streaming rendering
  • Otomatik code splitting

gibi avantajlar sunarak, React ekosistemini daha da güçlendiriyor.

Bu yaklaşımları kendi projelerinizde kullanarak:

  • Daha hızlı sayfa yüklemeleri
  • Daha iyi kullanıcı deneyimi
  • Daha az sunucu maliyeti
  • Daha kolay bakım
  • Daha iyi SEO

elde edebilirsiniz.

İlgili Etiketler: #React #ServerComponents #NextJS #WebDevelopment #Performance #Frontend #ReactPatterns #WebOptimization

Kaynaklar