React Design Patterns: Compound, Render Props, HOC

React Design Patterns: Compound, Render Props, HOC
React'te temiz, yeniden kullanılabilir ve bakımı kolay componentler yazmak için çeşitli tasarım kalıpları (design patterns) geliştirilmiştir. Bu rehberde, en yaygın ve etkili üç pattern'i — Compound Components, Render Props ve Higher-Order Components (HOC) — detaylı örneklerle inceleyeceğiz. Her pattern'in ne zaman kullanılacağını ve avantaj/dezavantajlarını ele alacağız.
1. Compound Components Pattern
Compound Components, birlikte çalışan ve bir parent component aracılığıyla implicit state paylaşan componentler oluşturmanızı sağlar. HTML'deki select/option ilişkisine benzer. Bu pattern, component API'sini esnek ve deklaratif tutar.
import React, { createContext, useContext, useState, ReactNode } from 'react';
// Accordion için shared context
interface AccordionContextType {
activeIndex: number | null;
toggleItem: (index: number) => void;
}
const AccordionContext = createContext<AccordionContextType | null>(null);
function useAccordionContext() {
const context = useContext(AccordionContext);
if (!context) throw new Error('Accordion compound componentleri Accordion içinde kullanılmalıdır');
return context;
}
// Ana Accordion component
function Accordion({ children, defaultIndex = null }: { children: ReactNode; defaultIndex?: number | null }) {
const [activeIndex, setActiveIndex] = useState<number | null>(defaultIndex);
const toggleItem = (index: number) => {
setActiveIndex((prev) => (prev === index ? null : index));
};
return (
<AccordionContext.Provider value={{ activeIndex, toggleItem }}>
<div className="border rounded-lg divide-y">{children}</div>
</AccordionContext.Provider>
);
}
// Accordion.Item compound component
function AccordionItem({ children, index }: { children: ReactNode; index: number }) {
return <div className="accordion-item">{children}</div>;
}
// Accordion.Header compound component
function AccordionHeader({ children, index }: { children: ReactNode; index: number }) {
const { activeIndex, toggleItem } = useAccordionContext();
const isOpen = activeIndex === index;
return (
<button
onClick={() => toggleItem(index)}
className="w-full flex justify-between items-center p-4 text-left font-medium"
aria-expanded={isOpen}
>
{children}
<span className={`transform transition-transform ${isOpen ? 'rotate-180' : ''}`}>▼</span>
</button>
);
}
// Accordion.Panel compound component
function AccordionPanel({ children, index }: { children: ReactNode; index: number }) {
const { activeIndex } = useAccordionContext();
if (activeIndex !== index) return null;
return (
<div className="p-4 bg-gray-50" role="region">
{children}
</div>
);
}
// Compound component'leri birleştir
Accordion.Item = AccordionItem;
Accordion.Header = AccordionHeader;
Accordion.Panel = AccordionPanel;
// Kullanım — çok temiz ve deklaratif
function FAQ() {
return (
<Accordion defaultIndex={0}>
<Accordion.Item index={0}>
<Accordion.Header index={0}>React nedir?</Accordion.Header>
<Accordion.Panel index={0}>
<p>React, Facebook tarafından geliştirilen bir JavaScript kütüphanesidir.</p>
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item index={1}>
<Accordion.Header index={1}>Hook nedir?</Accordion.Header>
<Accordion.Panel index={1}>
<p>Hook'lar, fonksiyon componentlerde state ve lifecycle kullanmanızı sağlar.</p>
</Accordion.Panel>
</Accordion.Item>
</Accordion>
);
}
2. Render Props Pattern
Render Props, bir component'in render etme mantığını dışarıdan (bir fonksiyon prop'u aracılığıyla) almasını sağlar. Bu pattern, component mantığını yeniden kullanmanın güçlü bir yoludur. Günümüzde custom hook'lar aynı amaca hizmet etse de, bazı senaryolarda hala çok kullanışlıdır.
import { useState, useEffect, ReactNode } from 'react';
// Render Props ile mouse pozisyon takibi
interface MousePosition {
x: number;
y: number;
}
interface MouseTrackerProps {
children: (position: MousePosition) => ReactNode;
}
function MouseTracker({ children }: MouseTrackerProps) {
const [position, setPosition] = useState<MousePosition>({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
return <>{children(position)}</>;
}
// Kullanım 1: Mouse koordinatlarını göster
function MouseDisplay() {
return (
<MouseTracker>
{({ x, y }) => (
<div className="fixed top-4 right-4 bg-black text-white px-3 py-1 rounded">
Mouse: {x}, {y}
</div>
)}
</MouseTracker>
);
}
// Kullanım 2: Mouse'u takip eden element
function FollowCursor() {
return (
<MouseTracker>
{({ x, y }) => (
<div
className="fixed w-6 h-6 bg-blue-500 rounded-full pointer-events-none"
style={{ left: x - 12, top: y - 12 }}
/>
)}
</MouseTracker>
);
}
3. Higher-Order Components (HOC)
HOC, bir component'i alıp, ona ekstra özellikler ekleyerek yeni bir component döndüren fonksiyondur. Fonksiyonel programlamadaki higher-order function kavramının React karşılığıdır. Cross-cutting concern'leri (auth, logging, error handling) merkezi olarak yönetmek için kullanılır.
import React, { ComponentType, useEffect, useState } from 'react';
// Auth HOC — kimlik doğrulama kontrolü
interface WithAuthProps {
user: { id: string; name: string; role: string } | null;
}
function withAuth<P extends object>(
WrappedComponent: ComponentType<P & WithAuthProps>,
requiredRole?: string
) {
return function AuthenticatedComponent(props: P) {
const [user, setUser] = useState<WithAuthProps['user']>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
async function checkAuth() {
try {
const res = await fetch('/api/auth/me');
if (res.ok) {
const userData = await res.json();
setUser(userData);
}
} finally {
setIsLoading(false);
}
}
checkAuth();
}, []);
if (isLoading) return <div>Yetki kontrol ediliyor...</div>;
if (!user) return <div>Giriş yapmanız gerekiyor. <a href="/login">Giriş Yap</a></div>;
if (requiredRole && user.role !== requiredRole) {
return <div>Bu sayfaya erişim yetkiniz yok.</div>;
}
return <WrappedComponent {...props} user={user} />;
};
}
// Loading HOC — veri yükleme durumu yönetimi
function withLoading<P extends { isLoading: boolean }>(
WrappedComponent: ComponentType<Omit<P, 'isLoading'>>
) {
return function LoadingComponent(props: P) {
const { isLoading, ...rest } = props;
if (isLoading) {
return (
<div className="flex justify-center items-center p-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" />
</div>
);
}
return <WrappedComponent {...(rest as any)} />;
};
}
// Kullanım
function AdminDashboard({ user }: WithAuthProps) {
return <h1>Hoşgeldiniz, {user?.name}!</h1>;
}
const ProtectedDashboard = withAuth(AdminDashboard, 'admin');
// JSX'te: <ProtectedDashboard />
Pattern Karşılaştırması
| Özellik | Compound Components | Render Props | HOC |
|---|---|---|---|
| Kullanım Alanı | UI component grupları | Mantık paylaşımı | Cross-cutting concerns |
| Karmaşıklık | Orta | Düşük | Yüksek |
| TypeScript Desteği | İyi | İyi | Zor |
| Modern Alternatif | Hala yaygın | Custom Hooks | Custom Hooks |
Sonuç
React design pattern'leri, component'lerinizi daha modüler, yeniden kullanılabilir ve test edilebilir kılar. Compound Components esnek UI API'leri oluşturmak için, Render Props mantığı dışarıdan enjekte etmek için, HOC ise cross-cutting concern'leri merkezi olarak yönetmek için kullanılır. Modern projelerde custom hook'lar birçok pattern'in yerini alsa da, Compound Components hala son derece güçlü ve yaygın bir pattern olarak varlığını sürdürmektedir.