İleri Düzey React Hooks: Desenler ve Optimizasyonlar
Kanıtlanmış desenlerle ileri düzey React Hooks'a hakim olun. Custom hooks, optimize useEffect, useMemo, useCallback ve performans teknikleri.

React Hooks, fonksiyonel bileşenlerde state ve yan etkilerin yönetim biçimini köklü şekilde değiştirdi. useState ve useEffect'in temel kullanımının ötesinde, ileri düzey desenler yeniden kullanılabilir, performanslı ve sürdürülebilir kod yazmayı mümkün kılar.
Bu rehber temel Hooks (useState, useEffect, useContext) bilgisini varsayar. Örnekler güçlü bir tipleme için React 18+ ve TypeScript kullanır.
useEffect'e hakim olmak: yaygın tuzaklardan kaçınmak
useEffect hook'u sıkça yanlış anlaşılır ve yanlış kullanılır. İşleyişini derinlemesine anlamak ince hataları ve performans sorunlarını önlemeye yardımcı olur.
Temel ilke şudur: useEffect bir bileşeni harici bir sistemle senkronize eder. Etki dış dünyayla (API, DOM, zamanlayıcı) iletişim kurmuyorsa, muhtemelen gereksizdir.
// Custom hook to synchronize document title
import { useEffect } from 'react'
export function useDocumentTitle(title: string) {
useEffect(() => {
// Save the previous title
const previousTitle = document.title
// Update the title
document.title = title
// Cleanup: restore previous title on unmount
return () => {
document.title = previousTitle
}
}, [title]) // Only trigger when title changes
}En yaygın hata, bağımlılık dizisini atlamak veya kararsız değerler eklemektir. Etki içinde kullanılan her değer bağımlılıklarda yer almalıdır.
'use client'
import { useEffect, useState } from 'react'
interface User {
id: string
name: string
email: string
}
interface UserProfileProps {
userId: string
}
export function UserProfile({ userId }: UserProfileProps) {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
// Flag to prevent state updates after unmount
let isMounted = true
// Controller to cancel pending requests
const controller = new AbortController()
async function fetchUser() {
setLoading(true)
setError(null)
try {
const response = await fetch(`/api/users/${userId}`, {
signal: controller.signal
})
if (!response.ok) {
throw new Error('User not found')
}
const data = await response.json()
// Check if component is still mounted
if (isMounted) {
setUser(data)
}
} catch (err) {
// Ignore abort errors
if (err instanceof Error && err.name !== 'AbortError') {
if (isMounted) {
setError(err.message)
}
}
} finally {
if (isMounted) {
setLoading(false)
}
}
}
fetchUser()
// Cleanup: abort request and mark as unmounted
return () => {
isMounted = false
controller.abort()
}
}, [userId]) // Re-run effect when userId changes
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error}</div>
if (!user) return null
return (
<div className="p-4 border rounded-lg">
<h2 className="text-xl font-bold">{user.name}</h2>
<p className="text-gray-600">{user.email}</p>
</div>
)
}AbortController ve isMounted bayrağıyla kurulan bu desen, bellek sızıntılarını ve unmount edilmiş bileşenler üzerindeki state güncellemelerini önler.
Yeniden kullanılabilir custom hooks oluşturma
Custom hooks, yeniden kullanılabilir mantığı kapsüller. İyi bir custom hook tek sorumluluk ilkesine uyar ve net bir API sunar.
// Generic hook to persist state in localStorage
import { useState, useEffect, useCallback } from 'react'
export function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: T | ((prev: T) => T)) => void, () => void] {
// Initialize state with stored value or default
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === 'undefined') {
return initialValue
}
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch (error) {
console.warn(`Error reading localStorage "${key}":`, error)
return initialValue
}
})
// Sync with localStorage on every change
useEffect(() => {
if (typeof window === 'undefined') return
try {
window.localStorage.setItem(key, JSON.stringify(storedValue))
} catch (error) {
console.warn(`Error writing localStorage "${key}":`, error)
}
}, [key, storedValue])
// Function to update value
const setValue = useCallback((value: T | ((prev: T) => T)) => {
setStoredValue((prev) => {
const nextValue = value instanceof Function ? value(prev) : value
return nextValue
})
}, [])
// Function to remove value
const removeValue = useCallback(() => {
setStoredValue(initialValue)
if (typeof window !== 'undefined') {
window.localStorage.removeItem(key)
}
}, [key, initialValue])
return [storedValue, setValue, removeValue]
}Bu hook, verileri kalıcı kılmak için her bileşende kullanılabilir.
'use client'
import { useLocalStorage } from '@/hooks/useLocalStorage'
type Theme = 'light' | 'dark' | 'system'
export function ThemeToggle() {
const [theme, setTheme] = useLocalStorage<Theme>('theme', 'system')
return (
<select
value={theme}
onChange={(e) => setTheme(e.target.value as Theme)}
className="px-3 py-2 border rounded-lg"
>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="system">System</option>
</select>
)
}Custom hooks her zaman "use" ile başlar (useLocalStorage, useFetch, useDebounce). Bu kural, React'in hook kurallarını doğrulamasına ve linting araçlarının doğru çalışmasına olanak tanır.
useReducer ile kompozisyon deseni
Birden çok eyleme sahip karmaşık state için useReducer, birden çok useState çağrısından daha öngörülebilir bir yapı sağlar.
// Cart management hook with useReducer
import { useReducer, useCallback, useMemo } from 'react'
interface CartItem {
id: string
name: string
price: number
quantity: number
}
interface CartState {
items: CartItem[]
isOpen: boolean
}
type CartAction =
| { type: 'ADD_ITEM'; payload: Omit<CartItem, 'quantity'> }
| { type: 'REMOVE_ITEM'; payload: string }
| { type: 'UPDATE_QUANTITY'; payload: { id: string; quantity: number } }
| { type: 'CLEAR_CART' }
| { type: 'TOGGLE_CART' }
function cartReducer(state: CartState, action: CartAction): CartState {
switch (action.type) {
case 'ADD_ITEM': {
const existingItem = state.items.find(
(item) => item.id === action.payload.id
)
if (existingItem) {
// Increment quantity if item exists
return {
...state,
items: state.items.map((item) =>
item.id === action.payload.id
? { ...item, quantity: item.quantity + 1 }
: item
)
}
}
// Add new item
return {
...state,
items: [...state.items, { ...action.payload, quantity: 1 }]
}
}
case 'REMOVE_ITEM':
return {
...state,
items: state.items.filter((item) => item.id !== action.payload)
}
case 'UPDATE_QUANTITY':
return {
...state,
items: state.items.map((item) =>
item.id === action.payload.id
? { ...item, quantity: action.payload.quantity }
: item
).filter((item) => item.quantity > 0)
}
case 'CLEAR_CART':
return { ...state, items: [] }
case 'TOGGLE_CART':
return { ...state, isOpen: !state.isOpen }
default:
return state
}
}
const initialState: CartState = {
items: [],
isOpen: false
}
export function useCart() {
const [state, dispatch] = useReducer(cartReducer, initialState)
// Memoized actions to avoid re-renders
const addItem = useCallback(
(item: Omit<CartItem, 'quantity'>) => {
dispatch({ type: 'ADD_ITEM', payload: item })
},
[]
)
const removeItem = useCallback((id: string) => {
dispatch({ type: 'REMOVE_ITEM', payload: id })
}, [])
const updateQuantity = useCallback((id: string, quantity: number) => {
dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } })
}, [])
const clearCart = useCallback(() => {
dispatch({ type: 'CLEAR_CART' })
}, [])
const toggleCart = useCallback(() => {
dispatch({ type: 'TOGGLE_CART' })
}, [])
// Memoized derived values
const total = useMemo(
() => state.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
),
[state.items]
)
const itemCount = useMemo(
() => state.items.reduce((sum, item) => sum + item.quantity, 0),
[state.items]
)
return {
items: state.items,
isOpen: state.isOpen,
total,
itemCount,
addItem,
removeItem,
updateQuantity,
clearCart,
toggleCart
}
}Bu yaklaşım tüm sepet mantığını merkezîleştirir ve birim testleri kolaylaştırır.
React / Next.js mülakatlarında başarılı olmaya hazır mısın?
İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.
useMemo ve useCallback ile optimizasyon
Bu hooks, gereksiz hesaplamaları veya fonksiyonların yeniden oluşturulmasını engeller. Kullanımları odaklı olmalıdır: her yere uygulamak performansı iyileştirmek yerine bozar.
'use client'
import { useState, useMemo, useCallback, memo } from 'react'
interface Product {
id: string
name: string
price: number
category: string
inStock: boolean
}
interface ProductListProps {
products: Product[]
}
// Memoized child component
const ProductCard = memo(function ProductCard({
product,
onAddToCart
}: {
product: Product
onAddToCart: (id: string) => void
}) {
console.log(`Render ProductCard: ${product.name}`)
return (
<div className="border rounded-lg p-4">
<h3 className="font-medium">{product.name}</h3>
<p className="text-gray-600">${product.price}</p>
<button
onClick={() => onAddToCart(product.id)}
disabled={!product.inStock}
className="mt-2 px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
>
{product.inStock ? 'Add to Cart' : 'Out of Stock'}
</button>
</div>
)
})
export function ProductList({ products }: ProductListProps) {
const [filter, setFilter] = useState('')
const [sortBy, setSortBy] = useState<'name' | 'price'>('name')
const [cart, setCart] = useState<string[]>([])
// useMemo: memoize result of expensive computation
const filteredAndSortedProducts = useMemo(() => {
console.log('Computing filteredAndSortedProducts')
let result = products
// Filter by name
if (filter) {
result = result.filter((p) =>
p.name.toLowerCase().includes(filter.toLowerCase())
)
}
// Sort
result = [...result].sort((a, b) => {
if (sortBy === 'name') {
return a.name.localeCompare(b.name)
}
return a.price - b.price
})
return result
}, [products, filter, sortBy]) // Recompute only when these deps change
// useCallback: memoize a function
const handleAddToCart = useCallback((productId: string) => {
setCart((prev) => [...prev, productId])
}, []) // Empty deps: function never changes
// Memoized statistics
const stats = useMemo(
() => ({
total: filteredAndSortedProducts.length,
inStock: filteredAndSortedProducts.filter((p) => p.inStock).length,
avgPrice:
filteredAndSortedProducts.reduce((sum, p) => sum + p.price, 0) /
filteredAndSortedProducts.length || 0
}),
[filteredAndSortedProducts]
)
return (
<div>
<div className="mb-4 flex gap-4">
<input
type="text"
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Search..."
className="px-3 py-2 border rounded-lg"
/>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as 'name' | 'price')}
className="px-3 py-2 border rounded-lg"
>
<option value="name">Name</option>
<option value="price">Price</option>
</select>
</div>
<p className="text-sm text-gray-600 mb-4">
{stats.total} products ({stats.inStock} in stock) -
Average price: ${stats.avgPrice.toFixed(2)}
</p>
<div className="grid grid-cols-3 gap-4">
{filteredAndSortedProducts.map((product) => (
<ProductCard
key={product.id}
product={product}
onAddToCart={handleAddToCart}
/>
))}
</div>
</div>
)
}Bu hooks karmaşıklığı artırır. Şunlarla sınırlı tutulmalıdır: (1) gerçekten maliyetli hesaplamalar, (2) memo() ile memoize edilmiş bileşenlere geçirilen prop'lar, (3) diğer hook'ların bağımlılıkları.
Arama alanları için debounce hook'u
Debounce, bir eylemin çok sık çalıştırılmasını engeller; tipik olarak bir arama alanına yazılırken kullanılır.
// Generic debounce hook
import { useState, useEffect } from 'react'
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value)
useEffect(() => {
// Create a timer that updates the value after delay
const timer = setTimeout(() => {
setDebouncedValue(value)
}, delay)
// Cleanup: cancel timer if value changes before delay
return () => {
clearTimeout(timer)
}
}, [value, delay])
return debouncedValue
}'use client'
import { useState, useEffect } from 'react'
import { useDebounce } from '@/hooks/useDebounce'
interface SearchResult {
id: string
title: string
}
export function SearchInput() {
const [query, setQuery] = useState('')
const [results, setResults] = useState<SearchResult[]>([])
const [loading, setLoading] = useState(false)
// Debounce the query by 300ms
const debouncedQuery = useDebounce(query, 300)
useEffect(() => {
// Don't search if query is empty
if (!debouncedQuery.trim()) {
setResults([])
return
}
async function search() {
setLoading(true)
try {
const response = await fetch(
`/api/search?q=${encodeURIComponent(debouncedQuery)}`
)
const data = await response.json()
setResults(data.results)
} catch (error) {
console.error('Search error:', error)
} finally {
setLoading(false)
}
}
search()
}, [debouncedQuery]) // Only triggered when debouncedQuery changes
return (
<div className="relative">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
className="w-full px-4 py-2 border rounded-lg"
/>
{loading && (
<div className="absolute right-3 top-2.5">
<div className="w-5 h-5 border-2 border-blue-600 border-t-transparent rounded-full animate-spin" />
</div>
)}
{results.length > 0 && (
<ul className="absolute w-full mt-1 bg-white border rounded-lg shadow-lg">
{results.map((result) => (
<li
key={result.id}
className="px-4 py-2 hover:bg-gray-100 cursor-pointer"
>
{result.title}
</li>
))}
</ul>
)}
</div>
)
}Bileşen yalnızca kullanıcı yazmayı bıraktıktan 300 ms sonra arama yapar; bu sayede onlarca gereksiz isteğin önüne geçer.
İleri düzey ref'ler için useImperativeHandle
Bu hook, bir alt bileşenin belirli yöntemlerini bir ref aracılığıyla üst bileşene açar.
'use client'
import {
useRef,
useImperativeHandle,
forwardRef,
useState,
useCallback
} from 'react'
// Interface of exposed methods
export interface VideoPlayerRef {
play: () => void
pause: () => void
seek: (time: number) => void
getCurrentTime: () => number
}
interface VideoPlayerProps {
src: string
poster?: string
}
export const VideoPlayer = forwardRef<VideoPlayerRef, VideoPlayerProps>(
function VideoPlayer({ src, poster }, ref) {
const videoRef = useRef<HTMLVideoElement>(null)
const [isPlaying, setIsPlaying] = useState(false)
// Expose only desired methods to parent
useImperativeHandle(
ref,
() => ({
play: () => {
videoRef.current?.play()
setIsPlaying(true)
},
pause: () => {
videoRef.current?.pause()
setIsPlaying(false)
},
seek: (time: number) => {
if (videoRef.current) {
videoRef.current.currentTime = time
}
},
getCurrentTime: () => {
return videoRef.current?.currentTime ?? 0
}
}),
[] // No deps: methods always use current ref
)
const handlePlayPause = useCallback(() => {
if (isPlaying) {
videoRef.current?.pause()
} else {
videoRef.current?.play()
}
setIsPlaying(!isPlaying)
}, [isPlaying])
return (
<div className="relative">
<video
ref={videoRef}
src={src}
poster={poster}
className="w-full rounded-lg"
onPlay={() => setIsPlaying(true)}
onPause={() => setIsPlaying(false)}
/>
<button
onClick={handlePlayPause}
className="absolute bottom-4 left-4 px-4 py-2 bg-black/50 text-white rounded"
>
{isPlaying ? 'Pause' : 'Play'}
</button>
</div>
)
}
)'use client'
import { useRef } from 'react'
import { VideoPlayer, VideoPlayerRef } from './VideoPlayer'
export function VideoController() {
const playerRef = useRef<VideoPlayerRef>(null)
const handleSkipForward = () => {
if (playerRef.current) {
const currentTime = playerRef.current.getCurrentTime()
playerRef.current.seek(currentTime + 10)
}
}
return (
<div className="space-y-4">
<VideoPlayer
ref={playerRef}
src="/videos/demo.mp4"
poster="/images/poster.jpg"
/>
<div className="flex gap-2">
<button
onClick={() => playerRef.current?.play()}
className="px-4 py-2 bg-green-600 text-white rounded"
>
Play
</button>
<button
onClick={() => playerRef.current?.pause()}
className="px-4 py-2 bg-red-600 text-white rounded"
>
Pause
</button>
<button
onClick={handleSkipForward}
className="px-4 py-2 bg-blue-600 text-white rounded"
>
+10s
</button>
</div>
</div>
)
}Bu desen özellikle medya bileşenleri, formlar veya emir niteliğinde kontrol gerektiren tüm arayüzler için faydalıdır.
İstek yaşam döngüsünü yöneten hook
Yükleme, hata ve veri durumlarını sıkı tipleme ile yönetmek için ileri düzey bir hook.
// Generic hook for HTTP requests
import { useState, useEffect, useCallback, useRef } from 'react'
interface UseFetchState<T> {
data: T | null
loading: boolean
error: Error | null
}
interface UseFetchOptions {
immediate?: boolean // Execute immediately on mount
onSuccess?: <T>(data: T) => void
onError?: (error: Error) => void
}
export function useFetch<T>(
url: string,
options: UseFetchOptions = {}
) {
const { immediate = true, onSuccess, onError } = options
const [state, setState] = useState<UseFetchState<T>>({
data: null,
loading: immediate,
error: null
})
// Ref to prevent updates after unmount
const mountedRef = useRef(true)
// Ref for abort controller
const abortControllerRef = useRef<AbortController | null>(null)
const execute = useCallback(async () => {
// Abort previous request if in progress
abortControllerRef.current?.abort()
abortControllerRef.current = new AbortController()
setState((prev) => ({ ...prev, loading: true, error: null }))
try {
const response = await fetch(url, {
signal: abortControllerRef.current.signal
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const data: T = await response.json()
if (mountedRef.current) {
setState({ data, loading: false, error: null })
onSuccess?.(data)
}
return data
} catch (error) {
// Ignore abort errors
if (error instanceof Error && error.name === 'AbortError') {
return null
}
const err = error instanceof Error ? error : new Error('Unknown error')
if (mountedRef.current) {
setState({ data: null, loading: false, error: err })
onError?.(err)
}
return null
}
}, [url, onSuccess, onError])
const reset = useCallback(() => {
setState({ data: null, loading: false, error: null })
}, [])
// Execute on mount if immediate=true
useEffect(() => {
if (immediate) {
execute()
}
return () => {
mountedRef.current = false
abortControllerRef.current?.abort()
}
}, [execute, immediate])
return {
...state,
execute,
reset,
isIdle: !state.loading && !state.data && !state.error
}
}Bu hook, otomatik iptal ve yaşam döngüsü yönetimi ile her isteği işleyebilen eksiksiz bir API sunar.
React / Next.js mülakatlarında başarılı olmaya hazır mısın?
İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.
Sonuç
İleri düzey React Hooks desenleri, sürdürülebilir ve performanslı kod oluşturmayı mümkün kılar. Akılda tutulması gereken anahtar noktalar:
- ✅ useEffect: cleanup'ı her zaman dahil edin ve race condition'ları AbortController ile yönetin
- ✅ Custom hooks: yeniden kullanılabilir mantığı net bir API ile kapsülleyin
- ✅ useReducer: birden çok eyleme sahip karmaşık state için tercih edin
- ✅ useMemo/useCallback: yalnızca gerektiğinde, hedefli biçimde kullanın
- ✅ useDebounce: arama gibi sık çalışan işlemleri sınırlayın
- ✅ useImperativeHandle: emir niteliğindeki yöntemleri kontrollü biçimde sunun
Bu desenlere hakim olmak, orta seviye React geliştiricilerini uzmanlardan ayırır. Her hook belirli bir sorunu çözer: doğru anda doğru aracı seçmek sağlam bir React mimarisinin anahtarıdır.
Pratik yapmaya başla!
Mülakat simülatörleri ve teknik testlerle bilgini test et.
Etiketler
Paylaş
İlgili makaleler

2026'da React Compiler: Otomatik Memoizasyon ve Teknik Mülakat Soruları
React Compiler hakkında kapsamlı rehber — otomatik memoizasyon, derleme hattı, React kuralları, ESLint entegrasyonu ve 2026 yılı React teknik mülakat soruları.

React 19: Produksiyonda Server Components - Eksiksiz Rehber
React 19 Server Components ile produksiyon ortaminda ustun performans. Mimari, kaliplar, streaming, onbellekleme ve optimizasyon teknikleri.

Üretimde React Server Components: desenler ve tuzaklar
Üretimde React Server Components: savaşta sınanmış desenler, sık karşılaşılan anti-desenler ve sağlam Next.js 15 uygulamaları için hata ayıklama stratejileri.