React: useRef والتلاعب بـ DOM
آخر تحديث: 2026-08-26
useRefإنه أشبه بـخزنة — يمكنك تخزين أي شيء بداخلها، وسيظل موجودًا عندما تخرجه. وعلى عكسuseState، فإن تغيير قيمةrefلن يؤدي إلى إعادة عرض المكون.
1. ما ستتعلمه
- الاستخدام الأساسي لـ useRef (مراجع DOM)
- تخزن
useRefالقيم القابلة للتغيير (دون إحداث إعادة عرض) - forwardRef: تمرير مرجع إلى مكون تابع
- تعرض useImperativeHandle أساليب المكونات الفرعية
- الاختيار بين useRef و useState
2. قصة شريط البحث
الاستخدامان الرئيسيان لـ useRef
flowchart LR
subgraph "Uses1: DOMQuote"
A[useRef] --> B["ref={inputRef}"]
B --> C["inputRef.current.focus()"]
end
subgraph "Uses2: Variable-Value Storage"
D[useRef] --> E[".current = value]
E --> F[Does not trigger a repaint]
end
G[forwardRef] --> H[Passed from the parent componentref]
H --> I[Child Component ExposureDOM]
style A fill:#e3f2fd,stroke:#1565c0
style D fill:#e8f5e9,stroke:#2e7d32
style G fill:#fff3e0,stroke:#e65100
(1) المشكلة: يتطلب التركيز على حقل الإدخال التعامل مع DOM
أنشأ بوب صفحة بحث وأراد أن يتم توجيه التركيز تلقائيًا إلى مربع البحث عند تحميل الصفحة:
function SearchPage() {
// ❌ Question:There is no direct way to focus on the input field.
return (
<div>
<input type="text" placeholder="Search..." />
{/* How can I make this input field automatically receive focus?? */}
{/* Cannot be used document.getElementById,React Direct manipulation is not recommended. DOM */}
</div>
)
}
يريد بوب استخدام useState لتنفيذ:
// ❌ Error:useState Updates trigger a re-render,But I don't know how to use it DOM
function SearchPage() {
const [input, setInput] = React.useState(null)
React.useEffect(() => {
// Hope input is DOM Element...
// But how do you DOM Element Storage state in ?
}, [])
return <input ref={el => setInput(el)} /> // Function ref,The grammar is strange
}
(2) حل باستخدام useRef
import { useRef, useEffect } from 'react'
function SearchPage() {
const inputRef = useRef(null) // Create ref
useEffect(() => {
// ✅ Automatically focus on the component after it is mounted
inputRef.current.focus()
}, [])
return <input ref={inputRef} type="text" placeholder="Autofocus search box..." />
}
المزايا: تُرجع useRef كائنًا قابلًا للتعديل { current: null }، و.current هو عنصر DOM حقيقي. ولا حاجة إلى محددات أو getElementById — فهي متوافقة بشكل طبيعي مع React.
3. الاستخدامان الأساسيان لـ useRef
| الغرض | ما تشير إليه ref.current | تؤدي إلى تحديث العرض | السيناريوهات الشائعة |
|---|---|---|---|
| مرجع DOM | عنصر DOM | ❌ | التركيز، التمرير، قياس الأبعاد |
| تخزين المتغيرات | أي قيمة في جافا سكريبت | ❌ | معرّف المؤقت، القيمة السابقة، حالة عدم العرض |
(1) حالة الاستخدام 1: مراجع DOM
الاستخدامات الأكثر شيوعًا — استرداد عناصر DOM، والتحكم في التركيز والحجم وموضع التمرير، وما إلى ذلك.
| العملية | الرمز | الوصف |
|---|---|---|
| التركيز | inputRef.current.focus() |
حقل إدخال التركيز التلقائي |
| تحديد النص | inputRef.current.select() |
تحديد الكل في حقل الإدخال |
| التمرير إلى | divRef.current.scrollIntoView() |
التمرير إلى عنصر معين |
| قراءة الأبعاد | divRef.current.offsetHeight |
الحصول على ارتفاع العنصر |
| تشغيل الفيديو | videoRef.current.play() |
التحكم في تشغيل الفيديو |
▶ مثال: 5 عمليات شائعة في DOM
// ============================================
// Example:useRef 's 5 Common Types DOM Operation
// ============================================
function DomOperations() {
const inputRef = useRef(null)
const videoRef = useRef(null)
const listRef = useRef(null)
const boxRef = useRef(null)
const [boxHeight, setBoxHeight] = React.useState(0)
// 1. Autofocus
useEffect(() => { inputRef.current.focus() }, [])
function handleSelect() { inputRef.current.select() } // 2. Select All
function handlePlay() { videoRef.current.play() } // 3. Play
function handlePause() { videoRef.current.pause() } // 3. Pause
function handleScroll() { listRef.current.scrollIntoView({ behavior: 'smooth' }) } // 4. Scroll
function handleMeasure() { // 5. Measure Dimensions
setBoxHeight(boxRef.current.offsetHeight)
}
return (
<div>
<h3>DOM Example of Operation</h3>
<input ref={inputRef} placeholder="Autofocus input field" />
<button onClick={handleSelect}>Select All Text</button>
<div ref={listRef} style={{ height: '100px', overflow: 'auto', border: '1px solid #ddd', margin: '10px 0' }}>
{Array.from({ length: 20 }, (_, i) => <p key={i}>Row {i + 1}</p>)}
</div>
<button onClick={handleScroll}>Scroll to the bottom</button>
<div ref={boxRef} style={{ padding: '20px', backgroundColor: '#f0f0f0', margin: '10px 0' }}>
<p>The height of this element is:{boxHeight}px</p>
<button onClick={handleMeasure}>Measure the height</button>
</div>
</div>
)
}
(2) حالة الاستخدام 2: تخزين قيم المتغيرات (دون إحداث إعادة رسم)
هذه هي الحيلة الخفية لـ useRef — فهي تخزن أي قيمة، ولن يتم إعادة عرض المكون عند تغير تلك القيمة.
function Stopwatch() {
const [time, setTime] = React.useState(0)
const timerRef = useRef(null) // Storage Timer ID,Do not participate in the rendering
function start() {
if (timerRef.current) return // Prevent Repeated Starts
timerRef.current = setInterval(() => {
setTime(t => t + 1)
}, 1000)
}
function stop() {
clearInterval(timerRef.current)
timerRef.current = null // Reset
}
return (
<div>
<p>Timer: {time}s</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
)
}
// timerRef Storage Timer ID
// Change timerRef.current Will not trigger a re-render
// The timer is not automatically cleared when the component is unloaded.(I remember back in useEffect Clean up)
4. useRef مقابل useState
| البعد | useRef | useState |
|---|---|---|
| قيمة الإرجاع | { current: initial } |
[value, setter] |
| طريقة التعديل | ref.current = newValue |
setState(newValue) |
| إعادة العرض بعد التعديل | ❌ لا يؤدي إلى إعادة العرض | ✅ يؤدي إلى إعادة العرض |
| عند قراءتها | تُقرأ أحدث قيمة على الفور | لا تُقرأ حتى عملية العرض التالية |
| حالات الاستخدام | عمليات DOM، معرّفات المؤقتات، القيم السابقة | البيانات المراد عرضها في واجهة المستخدم |
function RefVsState() {
const renderCount = useRef(1) // A counter that won't be triggered
const [count, setCount] = useState(0) // Counters that will be triggered
useEffect(() => {
renderCount.current += 1 // Change it however you like,Does not trigger a repaint
})
return (
<div>
<p>useState:{count}(Click to trigger rendering)</p>
<p>useRef:{renderCount.current}(View only in the console)</p>
<button onClick={() => setCount(c => c + 1)}>useState +1</button>
<button onClick={() => renderCount.current += 1}>useRef +1(No change)</button>
</div>
)
}
▶ مثال: استخدام useRef لحفظ القيمة السابقة
// ============================================
// Example:Go back to props/state value
// ============================================
function usePrevious(value) {
const ref = useRef()
useEffect(() => {
ref.current = value // Update after each render
})
return ref.current // Go back to the previous value
}
function Counter() {
const [count, setCount] = useState(0)
const prevCount = usePrevious(count) // The previous value
return (
<div>
<p>Currently:{count}</p>
<p>Last time:{prevCount !== undefined ? prevCount : '(For the first time)'}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
)
}
// Process:
// 1. Initial Rendering:count=0, prevCount=undefined
// 2. Click +1 → count=1
// 3. useEffect Update prev=0 → Back 0
// 4. Click again +1 → count=2, prev=1
5. forwardRef: المكونات الأصلية تتحكم في DOM للمكونات الفرعية
بشكل افتراضي، لا تكشف مكونات الوظائف عن مراجع DOM الخاصة بها. ويتيح استخدام forwardRef للمكونات الفرعية الحصول على ref الخاص بالمكون الأصلي:
// ---- Child component:use forwardRef Package ----
const CustomInput = forwardRef(function CustomInput(props, ref) {
return (
<div style={{ border: '1px solid #d9d9d9', padding: '4px', borderRadius: '4px' }}>
<input ref={ref} {...props} style={{ border: 'none', outline: 'none', width: '100%' }} />
</div>
)
})
// ---- Parent Component:Use directly ref Controlling subcomponents input ----
function Form() {
const inputRef = useRef(null)
useEffect(() => {
inputRef.current.focus() // ✅ Can focus directly on CustomInput Internal input
}, [])
return (
<div>
<CustomInput ref={inputRef} placeholder="Input fields controlled by the parent component" />
<button onClick={() => inputRef.current.focus()}>In Focus</button>
<button onClick={() => inputRef.current.select()}>Select All</button>
</div>
)
}
6. useImperativeHandle: إتاحة طريقة معينة
إذا كنت لا ترغب في إتاحة عنصر DOM بالكامل، فاستخدم useImperativeHandle لإتاحة طرق محددة فقط:
// ============================================
// Example:Custom Player——Expose only play/pause,Without revealing the whole video
// ============================================
const VideoPlayer = forwardRef(function VideoPlayer({ src }, ref) {
const videoRef = useRef(null)
// Expose only 3 A method for the parent component
useImperativeHandle(ref, () => ({
play() {
videoRef.current.play()
},
pause() {
videoRef.current.pause()
},
jumpTo(seconds) {
videoRef.current.currentTime = seconds
}
}))
return <video ref={videoRef} src={src} controls style={{ width: '100%' }} />
})
// ---- Usage -----
function App() {
const playerRef = useRef(null)
return (
<div>
<h3>Video Player</h3>
<VideoPlayer ref={playerRef} src="https://example.com/video.mp4" />
<div style={{ marginTop: '8px', display: 'flex', gap: '8px' }}>
<button onClick={() => playerRef.current.play()}>▶ Play</button>
<button onClick={() => playerRef.current.pause()}>⏸ Pause</button>
<button onClick={() => playerRef.current.jumpTo(30)}>⏭ Jump to 30s</button>
</div>
{/* The parent component cannot be manipulated directly video DOM,You can only call the exposed play/pause/jumpTo */}
</div>
)
}
7. مثال كامل: محرر النص المنسق
// ============================================
// Complete Example:Simple Rich Text Editor
// Features:useRef Control contentEditable Region
// forwardRef + useImperativeHandle Exposure Methods
// ============================================
const RichEditor = forwardRef(function RichEditor({ placeholder }, ref) {
const editorRef = useRef(null)
const [isEmpty, setIsEmpty] = React.useState(true)
// Exposing Methods to the Parent Component
useImperativeHandle(ref, () => ({
getContent() {
return editorRef.current.innerHTML
},
setContent(html) {
editorRef.current.innerHTML = html
checkEmpty()
},
clear() {
editorRef.current.innerHTML = ''
setIsEmpty(true)
editorRef.current.focus()
},
focus() {
editorRef.current.focus()
}
}))
function checkEmpty() {
const text = editorRef.current.textContent || ''
setIsEmpty(text.trim().length === 0)
}
function handleKeyDown(e) {
if (e.ctrlKey && e.key === 'b') {
document.execCommand('bold')
e.preventDefault()
}
if (e.ctrlKey && e.key === 'i') {
document.execCommand('italic')
e.preventDefault()
}
}
return (
<div style={{ border: '1px solid #d9d9d9', borderRadius: '4px', overflow: 'hidden' }}>
{/* Toolbar */}
<div style={{ padding: '8px', borderBottom: '1px solid #d9d9d9', backgroundColor: '#fafafa', display: 'flex', gap: '4px' }}>
<button onMouseDown={e => { e.preventDefault(); document.execCommand('bold') }} style={toolBtnStyle}><b>B</b></button>
<button onMouseDown={e => { e.preventDefault(); document.execCommand('italic') }} style={toolBtnStyle}><i>I</i></button>
<button onMouseDown={e => { e.preventDefault(); document.execCommand('underline') }} style={toolBtnStyle}><u>U</u></button>
<span style={{ color: '#ddd' }}>|</span>
<button onMouseDown={e => { e.preventDefault(); document.execCommand('insertUnorderedList') }} style={toolBtnStyle}>List</button>
<button onMouseDown={e => { e.preventDefault(); document.execCommand('formatBlock', false, 'h2') }} style={toolBtnStyle}>H2</button>
</div>
{/* Edit Area */}
<div
ref={editorRef}
contentEditable
onInput={checkEmpty}
onKeyDown={handleKeyDown}
style={{
minHeight: '200px',
padding: '16px',
outline: 'none',
lineHeight: '1.6'
}}
data-placeholder={placeholder}
{...(isEmpty ? { 'data-empty': 'true' } : {})}
/>
</div>
)
})
function App() {
const editorRef = useRef(null)
const [savedContent, setSavedContent] = React.useState('')
function handleSave() {
const content = editorRef.current.getContent()
setSavedContent(content)
alert('Saved!')
}
function handleClear() {
editorRef.current.clear()
}
function handleLoad() {
editorRef.current.setContent('<h2>Loaded content</h2><p>This is loaded from an external source. HTML。</p>')
}
return (
<div style={{ maxWidth: '700px', margin: '0 auto' }}>
<h2>📝 Rich Text Editor</h2>
<RichEditor ref={editorRef} placeholder="Start Writing..." />
<div style={{ marginTop: '12px', display: 'flex', gap: '8px' }}>
<button onClick={handleSave} style={btnStyle('#1890ff')}>💾 Save</button>
<button onClick={handleLoad} style={btnStyle('#52c41a')}>📂 Loading Example</button>
<button onClick={handleClear} style={btnStyle('#ff4d4f')}>🗑 Clear</button>
</div>
{savedContent && (
<div style={{ marginTop: '16px', padding: '16px', backgroundColor: '#f5f5f5', borderRadius: '4px' }}>
<p style={{ fontWeight: 'bold', margin: '0 0 8px 0' }}>Saved Content:</p>
<div style={{ fontSize: '13px', color: '#666', wordBreak: 'break-all', fontFamily: 'monospace' }}>
{savedContent}
</div>
</div>
)}
</div>
)
}
const toolBtnStyle = {
padding: '4px 10px', border: '1px solid transparent',
borderRadius: '3px', backgroundColor: 'transparent',
cursor: 'pointer', fontSize: '14px'
}
const btnStyle = (color) => ({
padding: '8px 16px', backgroundColor: color,
color: 'white', border: 'none',
borderRadius: '4px', cursor: 'pointer'
})
الناتج المتوقع: محرر نص منسق يدعم الخط العريض والمائل والتسطير والقوائم والعناوين من النوع H2 وخيارات التنسيق الأخرى؛ ويمكن حفظ المحتوى أو تحميله أو مسحه خارجيًا عبر
ref.
▶ المثال 3: استخدام useRef لإغلاق نافذة منبثقة عند النقر عليها من خارجها
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
}
function Dropdown() {
const [open, setOpen] = useState(false)
const dropdownRef = useClickOutside(() => setOpen(false))
return (
<div ref={dropdownRef} style={{ position: 'relative', display: 'inline-block' }}>
<button onClick={() => setOpen(!open)} style={{ padding: '8px 16px', cursor: 'pointer' }}>
Menu ▾
</button>
{open && (
<div style={{ position: 'absolute', top: '100%', left: 0, background: 'white', border: '1px solid #ddd', borderRadius: 4, minWidth: 120, boxShadow: '0 2px 8px rgba(0,0,0,0.15)' }}>
{['Profile', 'Settings', 'Logout'].map(item => (
<div key={item} onClick={() => { setOpen(false) }}
style={{ padding: '8px 16px', cursor: 'pointer' }}>
{item}
</div>
))}
</div>
)}
</div>
)
}
▶ المثال 4: استخدام useRef لتخزين مؤقت من أجل تنفيذ زر محدود السرعة
function ThrottledButton() {
const lastClickRef = useRef(0)
const [clicks, setClicks] = useState(0)
const [feedback, setFeedback] = useState('')
function handleClick() {
const now = Date.now()
if (now - lastClickRef.current < 1000) {
setFeedback('Too fast! Wait 1 second.')
return
}
lastClickRef.current = now
setClicks(c => c + 1)
setFeedback('Clicked!')
setTimeout(() => setFeedback(''), 500)
}
return (
<div style={{ textAlign: 'center', padding: 20 }}>
<button onClick={handleClick}
style={{ padding: '10px 24px', fontSize: 16, cursor: 'pointer' }}>
Click Me
</button>
<p>Clicks: {clicks}</p>
{feedback && <p style={{ color: feedback.includes('Too') ? '#ff4d4f' : '#52c41a' }}>{feedback}</p>}
</div>
)
}
▶ المثال 5: استخدام forwardRef لتغليف مكون Input القابل للتركيز
const FancyInput = React.forwardRef(function FancyInput({ label, error, ...props }, ref) {
const internalRef = useRef(null)
useImperativeHandle(ref, () => ({
focus: () => internalRef.current?.focus(),
selectAll: () => {
const el = internalRef.current
if (el) { el.focus(); el.select() }
},
scrollIntoView: () => internalRef.current?.scrollIntoView({ behavior: 'smooth' }),
}))
return (
<div style={{ marginBottom: 12 }}>
<label style={{ display: 'block', marginBottom: 4, fontSize: 14, fontWeight: 500 }}>{label}</label>
<input
ref={internalRef}
style={{ width: '100%', padding: 8, borderRadius: 4, border: `1px solid ${error ? '#ff4d4f' : '#d9d9d9'}`, outline: 'none' }}
{...props}
/>
{error && <p style={{ color: '#ff4d4f', fontSize: 12, margin: '4px 0 0' }}>{error}</p>}
</div>
)
})
function FormWithFancyInput() {
const emailRef = useRef(null)
function handleSubmit(e) {
e.preventDefault()
emailRef.current?.focus()
}
return (
<form onSubmit={handleSubmit} style={{ maxWidth: 400, margin: '0 auto' }}>
<h3>Sign Up</h3>
<FancyInput ref={emailRef} label="Email" type="email" placeholder="you@example.com" />
<FancyInput label="Password" type="password" placeholder="At least 6 characters" />
<button type="submit" style={{ padding: '8px 24px', cursor: 'pointer' }}>Submit</button>
</form>
)
}
❓ أسئلة شائعة
useRef أن يحل محل useState؟useRef لا تؤدي إلى إعادة عرض المكون، في حين أن setState يؤدي إلى ذلك. إذا كانت القيمة الخاصة بك تحتاج إلى أن تُعرض في واجهة المستخدم أو تؤثر على نتيجة العرض، فيجب عليك استخدام useState. إذا كنت تقوم ببساطة بتخزين «بيانات لا يلزم عرضها للمستخدم» (مثل معرّف الفاصل الزمني، أو القيمة من عملية العرض السابقة، أو مثيل WebSocket)، فاستخدم useRef. اعتبر useRef بمثابة «حاوية متغيرات لا تؤدي إلى إعادة عرض».📖 ملخص
useRefتعود إلى{ current: initialValue }؛ وتغيير.currentلا يؤدي إلى إعادة العرض- استخدامان: ① مراجع DOM (التركيز، الحجم، التمرير) ② تخزين قيم المتغيرات (معرف المؤقت، القيمة السابقة)
forwardRef: السماح لمكون أبوي بالتحكم في DOM لمكون تابع عبر مرجع (ref)useImperativeHandle: طريقة للحد من التعرض من خلال توفير واجهات برمجة التطبيقات (APIs) التي يحتاجها المكون الأصلي فقط- تحقق دائمًا من عدم وجود قيمة فارغة قبل إجراء أي عملية على ref:
ref.current?.focus()
📝 تمارين
- تمرين أساسي (مستوى الصعوبة ⭐): أنشئ مكونًا باسم
AutoFocusInputيتلقى التركيز تلقائيًّا عند تركيبه، وقم بتوفير طريقتين —focus()وclear()— ليتم استدعاؤهما من قبل المكون الأصلي. - تمرين متقدم (مستوى الصعوبة ⭐⭐): أنشئ مكونًا باسم
ClickCounter، واستخدمuseRefلتتبع عدد النقرات (دون عرضها في واجهة المستخدم)، واستخدمuseEffectلتسجيل عبارة "تم النقر X مرات" في وحدة التحكم عند كل عملية عرض. - التحدي (الصعوبة: ⭐⭐⭐): قم بإنشاء مكون
InfiniteScrollيستخدمuseRefلرصد دخول العنصر الحارس (عنصر معين في أسفل الصفحة) إلى منطقة العرض، مما يؤدي إلى تشغيل عملية تحميل المزيد من البيانات. استخدم واجهة برمجة التطبيقاتIntersectionObserver.