React: الخطافات المخصصة
آخر تحديث: 2026-08-26
1. ما ستتعلمه
- قواعد وأساليب تسمية الخطافات المخصصة
- تصميم معلمات وقيم الإرجاع للـ«هوكات» المخصصة
- 6 خطافات عملية مصنوعة حسب الطلب
- مبادئ إعادة استخدام الخطافات المخصصة
2. قصة أحد متطلبات إعادة استخدام النماذج
أنماط إعادة الاستخدام للخطافات المخصصة
flowchart TD
A[Repetitive Logic] --> B[Extract as useXxx]
B --> C[ComponentsA useXxx]
B --> D[ComponentsB useXxx]
B --> E[ComponentsC useXxx]
F[useXxxInside] --> G[useState]
F --> H[useEffect]
F --> I[useRef]
F --> J[useContext]
K[The Rule of Three] --> L{Number of logical repetitions?}
L -->|1 time| M[No extraction required]
L -->|2 times| N[Consider extracting]
L -->|3+ times| O[Must be extracted ✅]
style B fill:#e8f5e9,stroke:#2e7d32
style O fill:#c8e6c9,stroke:#2e7d32
(1) المشكلة: يتكرر نفس المنطق على مدى 5 صفحات
يحتاج بوب إلى «استرداد تفضيلات المستخدم من التخزين المحلي» في خمس صفحات مختلفة:
JSX
// Page A:Settings Page
function SettingsPage() {
const [prefs, setPrefs] = useState(() => {
const saved = localStorage.getItem('preferences')
return saved ? JSON.parse(saved) : { theme: 'light', lang: 'zh' }
})
useEffect(() => {
localStorage.setItem('preferences', JSON.stringify(prefs))
}, [prefs])
// ... Page A specifically UI
}
// Page B:Dashboard
function Dashboard() {
const [prefs, setPrefs] = useState(() => {
const saved = localStorage.getItem('preferences')
return saved ? JSON.parse(saved) : { theme: 'light', lang: 'zh' }
})
useEffect(() => {
localStorage.setItem('preferences', JSON.stringify(prefs))
}, [prefs])
// ... Page B specifically UI(Exactly the same logic!)
}
المشكلة: localStorage تم نسخ منطق القراءة/الكتابة ولصقه خمس مرات عبر خمس صفحات. إذا أردنا إضافة ميزة «رصد أحداث التخزين»، فسيتعين علينا تعديل خمسة ملفات.
(2) حلول للخطافات المخصصة
JSX
// ---- Custom Hook:Packaging localStorage Reading and Writing ----
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const saved = localStorage.getItem(key)
return saved ? JSON.parse(saved) : initialValue
} catch {
return initialValue
}
})
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(value))
} catch (e) {
console.error('Save to localStorage Failure:', e)
}
}, [key, value])
return [value, setValue]
}
// ---- Use this consistently on all pages ----
function SettingsPage() {
const [prefs, setPrefs] = useLocalStorage('preferences', {
theme: 'light', lang: 'zh'
})
// ... Specifically UI
}
function Dashboard() {
const [prefs, setPrefs] = useLocalStorage('preferences', {
theme: 'light', lang: 'zh'
})
// ... Specifically UI
}
المزايا: 20 سطراً من المنطق → سطر واحد من التعليمات البرمجية. عند تعديل المنطق، ما عليك سوى تغيير ملف واحد: useLocalStorage.
3. قواعد الخطافات المخصصة
| القاعدة | الوصف | عواقب المخالفة |
|---|---|---|
بدءًا من use |
تحديد الـ«هوكات» باستخدام أسماء تبعيات React | لا يتحقق «Linter» من هذه القاعدة؛ ولن تؤدي الاستدعاءات الشرطية إلى ظهور أخطاء |
| استدعاء الوظيفة في المستوى الأعلى فقط | لا تستدعِ الوظيفة داخل الحلقات أو العبارات الشرطية أو الكتل المتداخلة | عدم توافق سلسلة الـ«هوك»، والتشويش في الحالة |
| يُسمح بالاستدعاء فقط داخل مكونات الدالة أو الخطافات المخصصة | لا تستخدم داخل الدوال العادية أو مكونات الفئات | خطأ في وقت التشغيل: استدعاء خطاف غير صالح |
| تُرجع بيانات/دوال، وليس JSX | تُرجع «الهوكات» المنطق، بينما تُرجع المكونات واجهة المستخدم | تنتهك مبدأ فصل الاهتمامات، مما يجعل إعادة الاستخدام صعبة |
| معالجة الآثار الجانبية | استخدام دالة التنظيف في useEffect | تسربات الذاكرة، تراكم مستمعي الأحداث |
(1) قواعد تسمية الملفات
JSX
// ✅ It is essential to use Introduction
function useOnlineStatus() { }
function useWindowSize() { }
function useDebounce() { }
// ❌ Not based on use Introduction → React Will not check Hooks Rules
function onlineStatus() { }
function fetchData() { }
(2) تصميم قيمة الإرجاع
JSX
// Method 1:Return an array(like useState)
function useToggle(initial = false) {
const [value, setValue] = useState(initial)
const toggle = useCallback(() => setValue(v => !v), [])
return [value, toggle] // Array,Flexible Deconstruction of Names
}
// Method 2:Return Object(Greater clarity when there are multiple return values)
function useUser() {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
return { user, loading, error } // Object,Property Names Are Clear
}
// Method 3:Returns a single value
function useDocumentTitle(title) {
useEffect(() => { document.title = title }, [title])
// No return value required
}
(3) يمكن استخدام أي «hooks» داخليًّا
JSX
function useData(url) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
let cancelled = false
setLoading(true)
fetch(url)
.then(res => {
if (!res.ok) throw new Error('Request Failed')
return res.json()
})
.then(data => { if (!cancelled) { setData(data); setLoading(false) }})
.catch(err => { if (!cancelled) { setError(err.message); setLoading(false) }})
return () => { cancelled = true }
}, [url])
return { data, loading, error }
}
// Used internally useState + useEffect + useCallback(If necessary)
// Custom Hook You can combine any of the built-in features Hooks
4. 6 خطافات مخصصة عملية
| الخطاف | الوظيفة | الخطافات المستخدمة داخليًّا | القيمة المرجعة |
|---|---|---|---|
useToggle |
مفتاح تبديل منطقي | useState، useCallback |
{ value, toggle, setTrue, setFalse } |
useDebounce |
مدخلات مقاومة الاهتزاز | useState، useEffect |
debouncedValue |
useWindowSize |
مراقبة حجم النافذة | useState، useEffect |
{ width, height } |
useFetch |
طلب بيانات ثلاثي الحالات | useState، useEffect، useCallback |
{ data, loading, error, refetch } |
useIntersectionObserver |
كشف الرؤية | useState، useEffect |
isIntersecting |
useMediaQuery |
نقطة توقف التجاوب | useState، useEffect |
matches |
(1) العنصر 1: useToggle (مفتاح التبديل)
JSX
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue)
const toggle = useCallback(() => setValue(v => !v), [])
const setTrue = useCallback(() => setValue(true), [])
const setFalse = useCallback(() => setValue(false), [])
return { value, toggle, setTrue, setFalse }
}
// Usage
function ModalExample() {
const { value: isOpen, setTrue: open, setFalse: close } = useToggle(false)
return (
<div>
<button onClick={open}>Open the pop-up window</button>
{isOpen && (
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ backgroundColor: 'white', padding: '24px', borderRadius: '8px' }}>
<h2>Pop-up Title</h2>
<p>Pop-up Content</p>
<button onClick={close}>Close</button>
</div>
</div>
)}
</div>
)
}
(2) هوك 2: useDebounce (مضاد الاهتزاز)
JSX
function useDebounce(value, delay = 500) {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => clearTimeout(timer)
}, [value, delay])
return debouncedValue
}
// Usage:Search Input Stabilization
function SearchBox() {
const [query, setQuery] = useState('')
const debouncedQuery = useDebounce(query, 500)
// Only at debouncedQuery Request only when there is a change(The user has stopped typing 500ms after )
useEffect(() => {
if (debouncedQuery) {
fetch('/api/search?q=' + debouncedQuery).then(/* ... */)
}
}, [debouncedQuery])
return <input value={query} onChange={e => setQuery(e.target.value)} placeholder="Search..." />
}
(3) Hook 3: useWindowSize (حجم النافذة)
JSX
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
})
useEffect(() => {
let timeoutId
function handleResize() {
// Image Stabilization:50ms several times inside resize Execute only once
clearTimeout(timeoutId)
timeoutId = setTimeout(() => {
setSize({ width: window.innerWidth, height: window.innerHeight })
}, 50)
}
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
clearTimeout(timeoutId)
}
}, [])
return size
}
// Usage
function ResponsiveComponent() {
const { width } = useWindowSize()
return (
<p>
Current window width:{width}px
{width < 768 ? '(Mobile View)' : width < 1024 ? '(Flat View)' : '(Desktop View)'}
</p>
)
}
(4) الخطاف 4: useFetch (طلب البيانات)
JSX
function useFetch(url, options = {}) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
let cancelled = false
setLoading(true)
setError(null)
fetch(url, options)
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`)
return res.json()
})
.then(data => { if (!cancelled) { setData(data); setLoading(false) }})
.catch(err => { if (!cancelled) { setError(err.message); setLoading(false) }})
return () => { cancelled = true }
}, [url])
// Resubmit Request
const refetch = useCallback(() => {
setLoading(true)
fetch(url, options)
.then(res => res.json())
.then(data => { setData(data); setLoading(false) })
.catch(err => { setError(err.message); setLoading(false) })
}, [url])
return { data, loading, error, refetch }
}
// Usage
function UserProfile({ userId }) {
const { data: user, loading, error, refetch } = useFetch(`/api/users/${userId}`)
if (loading) return <p>Loading......</p>
if (error) return <p>Error:{error} <button onClick={refetch}>Retry</button></p>
return <h1>{user.name}</h1>
}
(5) النقطة الخامسة: استخدام IntersectionObserver (الكشف عن الرؤية)
JSX
function useIntersectionObserver(ref, options = {}) {
const [isIntersecting, setIsIntersecting] = useState(false)
useEffect(() => {
if (!ref.current) return
const observer = new IntersectionObserver(([entry]) => {
setIsIntersecting(entry.isIntersecting)
}, { threshold: options.threshold || 0.1, ...options })
observer.observe(ref.current)
return () => observer.disconnect()
}, [ref, options.threshold])
return isIntersecting
}
// Usage:Lazy Loading of Images
function LazyImage({ src, alt }) {
const imgRef = useRef(null)
const isVisible = useIntersectionObserver(imgRef)
const [loaded, setLoaded] = useState(false)
return (
<div ref={imgRef} style={{ minHeight: '200px', backgroundColor: '#f5f5f5' }}>
{isVisible && (
<img
src={src}
alt={alt}
onLoad={() => setLoaded(true)}
style={{ width: '100%', opacity: loaded ? 1 : 0, transition: 'opacity 0.3s' }}
/>
)}
</div>
)
}
(6) النقطة السادسة: useMediaQuery (نقاط التوقف التفاعلية)
JSX
function useMediaQuery(query) {
const [matches, setMatches] = useState(() => window.matchMedia(query).matches)
useEffect(() => {
const mql = window.matchMedia(query)
function handler(e) { setMatches(e.matches) }
mql.addEventListener('change', handler)
return () => mql.removeEventListener('change', handler)
}, [query])
return matches
}
// Usage
function ResponsiveLayout() {
const isMobile = useMediaQuery('(max-width: 768px)')
const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)')
const isDesktop = useMediaQuery('(min-width: 1025px)')
return (
<div>
{isMobile && <MobileMenu />}
{isDesktop && <FullSidebar />}
<p>
{isMobile ? '📱 Cell Phone' : isTablet ? '📟 Tablet' : '💻 Computer'}
</p>
</div>
)
}
5. مبادئ إعادة استخدام الخطافات المخصصة
| المبدأ | الشرح | مثال سيئ | مثال جيد |
|---|---|---|---|
| المسؤولية الواحدة | لا يقوم الـ«هوك» إلا بشيء واحد | useUserAndTheme() |
useUser() + useTheme() |
| المعلمات المرنة | توفير قيم افتراضية معقولة | useFetch('/api/data') عنوان URL مبرمج بشكل ثابت |
useFetch(url) عنوان URL معلماتي |
| قيم الإرجاع الكاملة | تُرجع ثلاث حالات: التحميل، والخطأ، والبيانات | useFetch() تُرجع البيانات فقط |
useFetch() تُرجع { data, loading, error } |
| تنظيف الموارد | تنظيف الآثار الجانبية عند إلغاء تثبيت المكون | عدم تنظيف المؤقتات/مستمعي الأحداث | تنظيف ما يرجعه useEffect |
| قابل للتركيب | يمكن دمج الخطافات الصغيرة لتشكيل خطافات أكبر | useComplexData() 100 سطر |
تركيبة useFetch() + useDebounce() |
▶ مثال: دمج عدة خطافات صغيرة
JSX
// ============================================
// Example:Combine multiple small Hook Building Complex Search Features
// ============================================
// 1. Small hook:Input Stabilization
function useDebounce(value, delay) { /* ... */ }
// 2. Small hook:Data Request
function useFetch(url) { /* ... */ }
// 3. Small hook:Local Storage
function useLocalStorage(key, initial) { /* ... */ }
// 4. Combined into a large Hook:Search
function useSearch(defaultQuery = '') {
const [query, setQuery] = useState(defaultQuery)
const [history, setHistory] = useLocalStorage('search_history', [])
const debouncedQuery = useDebounce(query, 500)
const { data, loading, error } = useFetch(
debouncedQuery ? `/api/search?q=${debouncedQuery}` : null
)
function search(newQuery) {
setQuery(newQuery)
// Save Search History
if (newQuery.trim()) {
setHistory(prev => [newQuery, ...prev.filter(h => h !== newQuery)].slice(0, 10))
}
}
return { query, setQuery: search, results: data, loading, error, history }
}
// useSearch Combined internally useState + useDebounce + useFetch + useLocalStorage
6. مثال كامل: معرض الصور (الجمع بين عدة هوكات مخصصة)
JSX
// ============================================
// Complete Example:Photo Gallery(Combination 3 Custom Hook)
// ============================================
import { useState, useEffect, useCallback, useRef } from 'react'
// ---- Custom Hook 1:Infinite Scrolling ----
function useInfiniteScroll(callback) {
const sentinelRef = useRef(null)
useEffect(() => {
const sentinel = sentinelRef.current
if (!sentinel) return
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) callback()
}, { threshold: 0.1 })
observer.observe(sentinel)
return () => observer.disconnect()
}, [callback])
return sentinelRef
}
// ---- Custom Hook 2:Keyboard Navigation ----
function useKeyPress(targetKey, callback) {
useEffect(() => {
function handler(e) {
if (e.key === targetKey) callback(e)
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
}, [targetKey, callback])
}
// ---- Custom Hook 3:Click here ----
function useClickOutside(callback) {
const ref = useRef(null)
useEffect(() => {
function handler(e) {
if (ref.current && !ref.current.contains(e.target)) {
callback()
}
}
document.addEventListener('mousedown', handler)
return () => document.removeEventListener('mousedown', handler)
}, [callback])
return ref
}
// ---- Image Gallery Component ----
function ImageGallery() {
const [images, setImages] = useState([])
const [page, setPage] = useState(1)
const [selected, setSelected] = useState(null)
const [loading, setLoading] = useState(false)
// Loading image
const loadMore = useCallback(() => {
if (loading) return
setLoading(true)
const newImages = Array.from({ length: 8 }, (_, i) => ({
id: images.length + i + 1,
url: `https://picsum.photos/seed/${images.length + i + 1}/300/200`,
title: `Image ${images.length + i + 1}`
}))
setTimeout(() => {
setImages(prev => [...prev, ...newImages])
setPage(p => p + 1)
setLoading(false)
}, 800)
}, [loading, images.length])
// First Load
useEffect(() => { loadMore() }, [])
// Infinite Scroll: Load More
const sentinelRef = useInfiniteScroll(loadMore)
// Click here to close the preview
const previewRef = useClickOutside(() => setSelected(null))
// Escape Key to close the preview
useKeyPress('Escape', () => setSelected(null))
// Use the left and right arrow keys to switch between images
useKeyPress('ArrowLeft', () => {
if (selected) {
const idx = images.findIndex(i => i.id === selected.id)
if (idx > 0) setSelected(images[idx - 1])
}
})
useKeyPress('ArrowRight', () => {
if (selected) {
const idx = images.findIndex(i => i.id === selected.id)
if (idx < images.length - 1) setSelected(images[idx + 1])
}
})
return (
<div style={{ maxWidth: '900px', margin: '0 auto' }}>
<h2>🖼️ Photo Gallery</h2>
<p style={{ color: '#666', fontSize: '13px' }}>Loaded {images.length} images | Keyboard:← Previous → Next Escape Close</p>
{/* Image Grid */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '12px' }}>
{images.map(img => (
<div key={img.id} onClick={() => setSelected(img)}
style={{ cursor: 'pointer', borderRadius: '8px', overflow: 'hidden', boxShadow: '0 2px 8px rgba(0,0,0,0.1)' }}>
<img src={img.url} alt={img.title}
style={{ width: '100%', height: '150px', objectFit: 'cover', transition: 'transform 0.3s' }}
onMouseEnter={e => e.currentTarget.style.transform = 'scale(1.05)'}
onMouseLeave={e => e.currentTarget.style.transform = 'scale(1)'} />
<p style={{ padding: '8px', margin: 0, fontSize: '13px', textAlign: 'center' }}>{img.title}</p>
</div>
))}
</div>
{/* Load More Triggers */}
<div ref={sentinelRef} style={{ textAlign: 'center', padding: '20px' }}>
{loading ? <p>⏳ Loading......</p> : <p style={{ color: '#999' }}>↓ Scroll to load more</p>}
</div>
{/* Image Preview(Click here to close) */}
{selected && (
<div style={{
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
backgroundColor: 'rgba(0,0,0,0.8)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
zIndex: 1000
}}>
<div ref={previewRef} style={{ maxWidth: '80vw', maxHeight: '80vh' }}>
<img src={selected.url.replace('300/200', '600/400')} alt={selected.title}
style={{ maxWidth: '100%', maxHeight: '80vh', borderRadius: '8px' }} />
<p style={{ color: 'white', textAlign: 'center', marginTop: '12px' }}>
{selected.title}(Click here to close)
</p>
</div>
</div>
)}
</div>
)
}
النتيجة المتوقعة: معرض صور يعرض 8 صور عند التحميل الأولي؛ ويؤدي التمرير إلى أسفل الصفحة إلى تحميل المزيد تلقائيًا؛ ويؤدي النقر على صورة إلى فتح نافذة معاينة (مع دعم التنقل باستخدام لوحة المفاتيح)؛ ويؤدي النقر خارج منطقة المعاينة إلى إغلاقها.
▶ المثال 2: هوك useClipboard — النسخ إلى الحافظة
JSX
function useClipboard() {
const [copied, setCopied] = useState(false)
async function copy(text) {
try {
await navigator.clipboard.writeText(text)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
} catch {
setCopied(false)
}
}
return { copied, copy }
}
function CopyButton({ text }) {
const { copied, copy } = useClipboard()
return (
<button onClick={() => copy(text)}
style={{ padding: '4px 12px', cursor: 'pointer', background: copied ? '#52c41a' : '#f0f0f0', color: copied ? 'white' : '#333', border: 'none', borderRadius: 4 }}>
{copied ? 'Copied!' : 'Copy'}
</button>
)
}
▶ المثال 3: ربط useOnlineStatus — الكشف عن حالة الشبكة
JSX
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine)
useEffect(() => {
function handleOnline() { setIsOnline(true) }
function handleOffline() { setIsOnline(false) }
window.addEventListener('online', handleOnline)
window.addEventListener('offline', handleOffline)
return () => {
window.removeEventListener('online', handleOnline)
window.removeEventListener('offline', handleOffline)
}
}, [])
return isOnline
}
function StatusBar() {
const isOnline = useOnlineStatus()
return (
<div style={{ padding: '8px 16px', background: isOnline ? '#f6ffed' : '#fff2f0', borderBottom: `1px solid ${isOnline ? '#b7eb8f' : '#ffccc7'}` }}>
{isOnline ? '🟢 Online' : '🔴 Offline — changes will sync when reconnected'}
</div>
)
}
▶ المثال 4: ربط useLocalStorage — حفظ الحالة
JSX
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const saved = localStorage.getItem(key)
return saved !== null ? JSON.parse(saved) : initialValue
} catch { return initialValue }
})
useEffect(() => {
try { localStorage.setItem(key, JSON.stringify(value)) }
catch (e) { console.error('localStorage save failed:', e) }
}, [key, value])
return [value, setValue]
}
function ThemeToggle() {
const [theme, setTheme] = useLocalStorage('theme', 'light')
return (
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}
style={{
padding: '8px 16px', cursor: 'pointer', borderRadius: 4, border: 'none',
background: theme === 'dark' ? '#333' : '#f0f0f0',
color: theme === 'dark' ? 'white' : '#333',
}}>
{theme === 'dark' ? '🌙 Dark' : '☀️ Light'} (saved)
</button>
)
}
▶ المثال 5: هوك useCountdown — العد التنازلي
JSX
function useCountdown(seconds, autostart = false) {
const [timeLeft, setTimeLeft] = useState(seconds)
const [isRunning, setIsRunning] = useState(autostart)
useEffect(() => {
if (!isRunning || timeLeft <= 0) return
const timer = setInterval(() => {
setTimeLeft(prev => {
if (prev <= 1) { setIsRunning(false); return 0 }
return prev - 1
})
}, 1000)
return () => clearInterval(timer)
}, [isRunning, timeLeft])
function start() { setTimeLeft(seconds); setIsRunning(true) }
function pause() { setIsRunning(false) }
function resume() { setIsRunning(true) }
const minutes = Math.floor(timeLeft / 60)
const secs = timeLeft % 60
const formatted = `${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`
return { timeLeft, formatted, isRunning, start, pause, resume }
}
function CountdownTimer() {
const { formatted, isRunning, start, pause, resume } = useCountdown(300)
return (
<div style={{ textAlign: 'center', padding: 20 }}>
<p style={{ fontSize: 48, fontFamily: 'monospace', margin: '0 0 16px' }}>{formatted}</p>
<div style={{ display: 'flex', gap: 8, justifyContent: 'center' }}>
{!isRunning ? <button onClick={start} style={{ padding: '8px 16px', cursor: 'pointer' }}>Start</button>
: <button onClick={pause} style={{ padding: '8px 16px', cursor: 'pointer' }}>Pause</button>}
{isRunning || <button onClick={resume} style={{ padding: '8px 16px', cursor: 'pointer' }}>Resume</button>}
</div>
</div>
)
}
❓ أسئلة شائعة
س ما الفرق بين هوك مخصص ودالة عادية؟
ج يمكنك استخدام هوكات React (مثل
useState وuseEffect) داخل هوك مخصص، ولكن لا يمكنك استخدامها داخل دالة عادية. لذلك، يجب استخدام هوك مخصص فقط لـ«المنطق القابل لإعادة الاستخدام الذي يتطلب استخدام هوكس»، في حين أن الدالة العادية كافية لـ«المنطق الحسابي البحت». على سبيل المثال، يستخدم formatDate() دالة عادية، بينما يستخدم useWindowSize() هوك مخصص.س كيف يمكن اختبار الخطافات المخصصة؟
ج استخدم مكتبة
@testing-library/react-hooks. اختبار الخطافات المخصصة يشبه تمامًا اختبار المكونات — قم بعرض الخطاف، وتحقق من القيمة المرجعة، وقم بتشغيل عملية التحديث، وتحقق من التغييرات. بالنسبة للخطافات المنطقية البحتة، يكون الاختبار بسيطًا للغاية.س متى ينبغي عليك استخراج المنطق إلى هوك مخصص؟
ج عندما ترى نفس المنطق في مكونين أو أكثر. لا تقم باستخراجه مبكرًا جدًّا (قبل أن تتأكد من أن المنطق سيُعاد استخدامه فعليًّا)، ولكن لا تنتظر طويلاً أيضًا (حتى تقوم بنسخه ولصقه خمس مرات). وهناك قاعدة عامة مفيدة تُعرف بـ«قاعدة الثلاث مرات» — إذا ظهر نفس المنطق ثلاث مرات أو أكثر، فيجب عليك استخراجه إلى هوك مخصص.
س هل يمكن استخدام
useEffect في هوك مخصص؟ج نعم، وهذا أمر شائع جدًّا. يمكن للهوكات المخصصة استخدام أي هوكات مدمجة (مثل
useState، useEffect، useRef، useContext، إلخ). على سبيل المثال، يستخدم هوك useFetch هوك useState داخليًّا لإدارة البيانات والتحميل وحالات الخطأ؛ ويستخدم useEffect لجلب البيانات عند التثبيت؛ ويستخدم useRef لتخزين AbortController. وهذه هي بالضبط قيمة الهوكات المخصصة — دمج هوكات متعددة في وحدة منطقية واحدة قابلة لإعادة الاستخدام.📖 ملخص
- الخطاف المخصص هو دالة تبدأ بـ
use؛ ويمكن استخدام أي خطافات مدمجة داخلها. - يمكن أن تكون قيمة الإرجاع مصفوفة (مثل
useState)، أو كائنًا (قيم إرجاع متعددة)، أو قيمة واحدة - 6 وظائف عملية: useToggle، useDebounce، useWindowSize، useFetch، useIntersectionObserver، useMediaQuery
- اتبع «قاعدة الثلاثة» — إذا تم إعادة استخدام جزء من الكود ثلاث مرات أو أكثر من الناحية المنطقية، فاستخرج هذا الجزء إلى «هوك»
- يمكن دمج الخطافات الصغيرة لتشكيل خطافات أكبر، وفقًا لمبدأ المسؤولية الواحدة
📝 تمارين
- المشكلة الأساسية (الصعوبة ⭐): أنشئ ربطًا
useCounterيُرجع{ count, increment, decrement, reset }، مع قيمة أولية قابلة للتعيين. - مشكلة متقدمة (درجة الصعوبة ⭐⭐): قم بإنشاء هوك
useClipboardيلتف حول واجهة برمجة التطبيقاتnavigator.clipboardويعيد{ copied, copy(text) }، مع دعم نسخ النص إلى الحافظة. - التحدي (الصعوبة: ⭐⭐⭐): قم بإنشاء دالة
useWebSocketتغلف اتصال WebSocket وتُرجع{ data, isConnected, send(message), reconnect() }، مع دعم إعادة الاتصال التلقائي واكتشاف نبض الاتصال.