Frontend Geliştirme Kalıpları
React, Next.js ve performanslı kullanıcı arayüzleri için modern frontend kalıpları.
Ne Zaman Aktifleştirmelisiniz
- React bileşenleri oluştururken (composition, props, rendering)
- State yönetirken (useState, useReducer, Zustand, Context)
- Veri çekme implementasyonu (SWR, React Query, server components)
- Performans optimize ederken (memoization, virtualization, code splitting)
- Formlarla çalışırken (validation, controlled inputs, Zod schemas)
- Client-side routing ve navigasyon işlerken
- Erişilebilir, responsive UI kalıpları oluştururken
Bileşen Kalıpları
Kalıtım Yerine Composition
typescript
// PASS: İYİ: Bileşen compositioninterface CardProps { children: React.ReactNode variant?: 'default' | 'outlined'}
export function Card({ children, variant = 'default' }: CardProps) { return <div className={`card card-${variant}`}>{children}</div>}
export function CardHeader({ children }: { children: React.ReactNode }) { return <div className="card-header">{children}</div>}
export function CardBody({ children }: { children: React.ReactNode }) { return <div className="card-body">{children}</div>}
// Kullanım<Card> <CardHeader>Başlık</CardHeader> <CardBody>İçerik</CardBody></Card>Compound Components
typescript
interface TabsContextValue { activeTab: string setActiveTab: (tab: string) => void}
const TabsContext = createContext<TabsContextValue | undefined>(undefined)
export function Tabs({ children, defaultTab }: { children: React.ReactNode defaultTab: string}) { const [activeTab, setActiveTab] = useState(defaultTab)
return ( <TabsContext.Provider value={{ activeTab, setActiveTab }}> {children} </TabsContext.Provider> )}
export function TabList({ children }: { children: React.ReactNode }) { return <div className="tab-list">{children}</div>}
export function Tab({ id, children }: { id: string, children: React.ReactNode }) { const context = useContext(TabsContext) if (!context) throw new Error('Tab must be used within Tabs')
return ( <button className={context.activeTab === id ? 'active' : ''} onClick={() => context.setActiveTab(id)} > {children} </button> )}
// Kullanım<Tabs defaultTab="overview"> <TabList> <Tab id="overview">Genel Bakış</Tab> <Tab id="details">Detaylar</Tab> </TabList></Tabs>Render Props Kalıbı
typescript
interface DataLoaderProps<T> { url: string children: (data: T | null, loading: boolean, error: Error | null) => React.ReactNode}
export function DataLoader<T>({ url, children }: DataLoaderProps<T>) { const [data, setData] = useState<T | null>(null) const [loading, setLoading] = useState(true) const [error, setError] = useState<Error | null>(null)
useEffect(() => { fetch(url) .then(res => res.json()) .then(setData) .catch(setError) .finally(() => setLoading(false)) }, [url])
return <>{children(data, loading, error)}</>}
// Kullanım<DataLoader<Market[]> url="/api/markets"> {(markets, loading, error) => { if (loading) return <Spinner /> if (error) return <Error error={error} /> return <MarketList markets={markets!} /> }}</DataLoader>Özel Hook Kalıpları
State Yönetimi Hook'u
typescript
export function useToggle(initialValue = false): [boolean, () => void] { const [value, setValue] = useState(initialValue)
const toggle = useCallback(() => { setValue(v => !v) }, [])
return [value, toggle]}
// Kullanımconst [isOpen, toggleOpen] = useToggle()Async Veri Çekme Hook'u
typescript
interface UseQueryOptions<T> { onSuccess?: (data: T) => void onError?: (error: Error) => void enabled?: boolean}
export function useQuery<T>( key: string, fetcher: () => Promise<T>, options?: UseQueryOptions<T>) { const [data, setData] = useState<T | null>(null) const [error, setError] = useState<Error | null>(null) const [loading, setLoading] = useState(false)
// Çağıranlar satır içi fonksiyonlar ve nesne literalleri geçirse bile // refetch'in referans olarak kararlı kalması için en güncel fetcher/options // değerlerini ref'lerde tutun. Bu olmadan her render yeni bir refetch // oluşturur ve aşağıdaki effect her state güncellemesinden sonra yeniden // çalışır - sonsuz bir fetch döngüsü. const fetcherRef = useRef(fetcher) const optionsRef = useRef(options) useEffect(() => { fetcherRef.current = fetcher optionsRef.current = options })
const refetch = useCallback(async () => { setLoading(true) setError(null)
try { const result = await fetcherRef.current() setData(result) optionsRef.current?.onSuccess?.(result) } catch (err) { const error = err as Error setError(error) optionsRef.current?.onError?.(error) } finally { setLoading(false) } }, [])
const enabled = options?.enabled !== false
useEffect(() => { if (enabled) { refetch() } }, [key, enabled, refetch])
return { data, error, loading, refetch }}
// Kullanımconst { data: markets, loading, error, refetch } = useQuery( 'markets', () => fetch('/api/markets').then(r => r.json()), { onSuccess: data => console.log('Getirilen', data.length, 'market'), onError: err => console.error('Başarısız:', err) })Debounce Hook'u
typescript
export function useDebounce<T>(value: T, delay: number): T { const [debouncedValue, setDebouncedValue] = useState<T>(value)
useEffect(() => { const handler = setTimeout(() => { setDebouncedValue(value) }, delay)
return () => clearTimeout(handler) }, [value, delay])
return debouncedValue}
// Kullanımconst [searchQuery, setSearchQuery] = useState('')const debouncedQuery = useDebounce(searchQuery, 500)
useEffect(() => { if (debouncedQuery) { performSearch(debouncedQuery) }}, [debouncedQuery])State Yönetimi Kalıpları
Context + Reducer Kalıbı
typescript
interface State { markets: Market[] selectedMarket: Market | null loading: boolean}
type Action = | { type: 'SET_MARKETS'; payload: Market[] } | { type: 'SELECT_MARKET'; payload: Market } | { type: 'SET_LOADING'; payload: boolean }
function reducer(state: State, action: Action): State { switch (action.type) { case 'SET_MARKETS': return { ...state, markets: action.payload } case 'SELECT_MARKET': return { ...state, selectedMarket: action.payload } case 'SET_LOADING': return { ...state, loading: action.payload } default: return state }}
const MarketContext = createContext<{ state: State dispatch: Dispatch<Action>} | undefined>(undefined)
export function MarketProvider({ children }: { children: React.ReactNode }) { const [state, dispatch] = useReducer(reducer, { markets: [], selectedMarket: null, loading: false })
return ( <MarketContext.Provider value={{ state, dispatch }}> {children} </MarketContext.Provider> )}
export function useMarkets() { const context = useContext(MarketContext) if (!context) throw new Error('useMarkets must be used within MarketProvider') return context}Performans Optimizasyonu
Memoization
typescript
// PASS: Pahalı hesaplamalar için useMemo// Sıralamadan önce kopyalayın - Array.prototype.sort yerinde değiştirirconst sortedMarkets = useMemo(() => { return [...markets].sort((a, b) => b.volume - a.volume)}, [markets])
// PASS: Alt bileşenlere geçirilen fonksiyonlar için useCallbackconst handleSearch = useCallback((query: string) => { setSearchQuery(query)}, [])
// PASS: Pure bileşenler için React.memoexport const MarketCard = React.memo<MarketCardProps>(({ market }) => { return ( <div className="market-card"> <h3>{market.name}</h3> <p>{market.description}</p> </div> )})Code Splitting ve Lazy Loading
typescript
import { lazy, Suspense } from 'react'
// PASS: Ağır bileşenleri lazy yükleconst HeavyChart = lazy(() => import('./HeavyChart'))const ThreeJsBackground = lazy(() => import('./ThreeJsBackground'))
export function Dashboard() { return ( <div> <Suspense fallback={<ChartSkeleton />}> <HeavyChart data={data} /> </Suspense>
<Suspense fallback={null}> <ThreeJsBackground /> </Suspense> </div> )}Uzun Listeler için Virtualization
typescript
import { useVirtualizer } from '@tanstack/react-virtual'
export function VirtualMarketList({ markets }: { markets: Market[] }) { const parentRef = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({ count: markets.length, getScrollElement: () => parentRef.current, estimateSize: () => 100, // Tahmini satır yüksekliği overscan: 5 // Ekstra render edilecek öğeler })
return ( <div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}> <div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }} > {virtualizer.getVirtualItems().map(virtualRow => ( <div key={virtualRow.index} style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: `${virtualRow.size}px`, transform: `translateY(${virtualRow.start}px)` }} > <MarketCard market={markets[virtualRow.index]} /> </div> ))} </div> </div> )}Form İşleme Kalıpları
Doğrulamalı Controlled Form
typescript
interface FormData { name: string description: string endDate: string}
interface FormErrors { name?: string description?: string endDate?: string}
export function CreateMarketForm() { const [formData, setFormData] = useState<FormData>({ name: '', description: '', endDate: '' })
const [errors, setErrors] = useState<FormErrors>({})
const validate = (): boolean => { const newErrors: FormErrors = {}
if (!formData.name.trim()) { newErrors.name = 'İsim gereklidir' } else if (formData.name.length > 200) { newErrors.name = 'İsim 200 karakterden az olmalıdır' }
if (!formData.description.trim()) { newErrors.description = 'Açıklama gereklidir' }
if (!formData.endDate) { newErrors.endDate = 'Bitiş tarihi gereklidir' }
setErrors(newErrors) return Object.keys(newErrors).length === 0 }
const handleSubmit = async (e: React.FormEvent) => { e.preventDefault()
if (!validate()) return
try { await createMarket(formData) // Başarı işleme } catch (error) { // Hata işleme } }
return ( <form onSubmit={handleSubmit}> <input value={formData.name} onChange={e => setFormData(prev => ({ ...prev, name: e.target.value }))} placeholder="Market ismi" /> {errors.name && <span className="error">{errors.name}</span>}
{/* Diğer alanlar */}
<button type="submit">Market Oluştur</button> </form> )}Error Boundary Kalıbı
typescript
interface ErrorBoundaryState { hasError: boolean error: Error | null}
export class ErrorBoundary extends React.Component< { children: React.ReactNode }, ErrorBoundaryState> { state: ErrorBoundaryState = { hasError: false, error: null }
static getDerivedStateFromError(error: Error): ErrorBoundaryState { return { hasError: true, error } }
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { console.error('Error boundary caught:', error, errorInfo) }
render() { if (this.state.hasError) { return ( <div className="error-fallback"> <h2>Bir şeyler yanlış gitti</h2> <p>{this.state.error?.message}</p> <button onClick={() => this.setState({ hasError: false })}> Tekrar dene </button> </div> ) }
return this.props.children }}
// Kullanım<ErrorBoundary> <App /></ErrorBoundary>Animasyon Kalıpları
Framer Motion Animasyonları
typescript
import { motion, AnimatePresence } from 'framer-motion'
// PASS: Liste animasyonlarıexport function AnimatedMarketList({ markets }: { markets: Market[] }) { return ( <AnimatePresence> {markets.map(market => ( <motion.div key={market.id} initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -20 }} transition={{ duration: 0.3 }} > <MarketCard market={market} /> </motion.div> ))} </AnimatePresence> )}
// PASS: Modal animasyonlarıexport function Modal({ isOpen, onClose, children }: ModalProps) { return ( <AnimatePresence> {isOpen && ( <> <motion.div className="modal-overlay" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} onClick={onClose} /> <motion.div className="modal-content" initial={{ opacity: 0, scale: 0.9, y: 20 }} animate={{ opacity: 1, scale: 1, y: 0 }} exit={{ opacity: 0, scale: 0.9, y: 20 }} > {children} </motion.div> </> )} </AnimatePresence> )}Erişilebilirlik Kalıpları
Klavye Navigasyonu
typescript
export function Dropdown({ options, onSelect }: DropdownProps) { const [isOpen, setIsOpen] = useState(false) const [activeIndex, setActiveIndex] = useState(0)
const handleKeyDown = (e: React.KeyboardEvent) => { switch (e.key) { case 'ArrowDown': e.preventDefault() setActiveIndex(i => Math.min(i + 1, options.length - 1)) break case 'ArrowUp': e.preventDefault() setActiveIndex(i => Math.max(i - 1, 0)) break case 'Enter': e.preventDefault() onSelect(options[activeIndex]) setIsOpen(false) break case 'Escape': setIsOpen(false) break } }
return ( <div role="combobox" aria-expanded={isOpen} aria-haspopup="listbox" onKeyDown={handleKeyDown} > {/* Dropdown implementasyonu */} </div> )}Focus Yönetimi
typescript
export function Modal({ isOpen, onClose, children }: ModalProps) { const modalRef = useRef<HTMLDivElement>(null) const previousFocusRef = useRef<HTMLElement | null>(null)
useEffect(() => { if (isOpen) { // Şu anki focus'lanmış elementi kaydet previousFocusRef.current = document.activeElement as HTMLElement
// Modal'a focus yap modalRef.current?.focus() } else { // Kapatırken focus'u geri yükle previousFocusRef.current?.focus() } }, [isOpen])
return isOpen ? ( <div ref={modalRef} role="dialog" aria-modal="true" tabIndex={-1} onKeyDown={e => e.key === 'Escape' && onClose()} > {children} </div> ) : null}Unutmayın: Modern frontend kalıpları sürdürülebilir, performanslı kullanıcı arayüzleri sağlar. Proje karmaşıklığınıza uyan kalıpları seçin.


