React: useRef والتلاعب بـ DOM

آخر تحديث: 2026-08-26

useRef إنه أشبه بـخزنة — يمكنك تخزين أي شيء بداخلها، وسيظل موجودًا عندما تخرجه. وعلى عكس useState، فإن تغيير قيمة ref لن يؤدي إلى إعادة عرض المكون.


1. ما ستتعلمه



2. قصة شريط البحث

الاستخدامان الرئيسيان لـ useRef

100%
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

أنشأ بوب صفحة بحث وأراد أن يتم توجيه التركيز تلقائيًا إلى مربع البحث عند تحميل الصفحة:

JSX
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 لتنفيذ:

JSX
// ❌ 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

JSX
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

JSX
// ============================================
// 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 — فهي تخزن أي قيمة، ولن يتم إعادة عرض المكون عند تغير تلك القيمة.

JSX
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، معرّفات المؤقتات، القيم السابقة البيانات المراد عرضها في واجهة المستخدم
JSX
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 لحفظ القيمة السابقة

JSX
// ============================================
// 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 الخاص بالمكون الأصلي:

JSX
// ---- 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 لإتاحة طرق محددة فقط:

JSX
// ============================================
// 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. مثال كامل: محرر النص المنسق

JSX
// ============================================
// 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 لإغلاق نافذة منبثقة عند النقر عليها من خارجها

JSX
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 لتخزين مؤقت من أجل تنفيذ زر محدود السرعة

JSX
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 القابل للتركيز

JSX
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>
  )
}
▶ جرّب الكود

❓ أسئلة شائعة

س هل يجب استخدام forwardRef في كل مكون؟
ج لا. استخدمه فقط في الحالات التالية: ① عندما يحتاج المكون الأصلي إلى التعامل مباشرةً مع DOM لمكون تابع (مثل توجيه التركيز إلى حقل إدخال أو التحكم في تشغيل الوسائط)؛ ② عند تغليف مكونات النماذج القابلة لإعادة الاستخدام (حتى يتمكن المكون الأصلي من التحكم في التركيز أو التحديد). إذا كنت تقوم فقط بتمرير البيانات، فإن استخدام props يكفي.
س هل يمكن لـ useRef أن يحل محل useState؟
ج لا. التغييرات التي تطرأ على useRef لا تؤدي إلى إعادة عرض المكون، في حين أن setState يؤدي إلى ذلك. إذا كانت القيمة الخاصة بك تحتاج إلى أن تُعرض في واجهة المستخدم أو تؤثر على نتيجة العرض، فيجب عليك استخدام useState. إذا كنت تقوم ببساطة بتخزين «بيانات لا يلزم عرضها للمستخدم» (مثل معرّف الفاصل الزمني، أو القيمة من عملية العرض السابقة، أو مثيل WebSocket)، فاستخدم useRef. اعتبر useRef بمثابة «حاوية متغيرات لا تؤدي إلى إعادة عرض».

📖 ملخص


📝 تمارين

  1. تمرين أساسي (مستوى الصعوبة ⭐): أنشئ مكونًا باسم AutoFocusInput يتلقى التركيز تلقائيًّا عند تركيبه، وقم بتوفير طريقتين — focus() وclear() — ليتم استدعاؤهما من قبل المكون الأصلي.
  2. تمرين متقدم (مستوى الصعوبة ⭐⭐): أنشئ مكونًا باسم ClickCounter، واستخدم useRef لتتبع عدد النقرات (دون عرضها في واجهة المستخدم)، واستخدم useEffect لتسجيل عبارة "تم النقر X مرات" في وحدة التحكم عند كل عملية عرض.
  3. التحدي (الصعوبة: ⭐⭐⭐): قم بإنشاء مكون InfiniteScroll يستخدم useRef لرصد دخول العنصر الحارس (عنصر معين في أسفل الصفحة) إلى منطقة العرض، مما يؤدي إلى تشغيل عملية تحميل المزيد من البيانات. استخدم واجهة برمجة التطبيقات IntersectionObserver.
Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%