← React/useRef: DOM-доступ и мутабельные значения#266 из 383← ПредыдущийСледующий →+20 XP
Полезно по теме:Гайд: React или VueПрактика: React setТермин: React HooksТема: React: хуки и экосистема

useRef: DOM-доступ и мутабельные значения

Что такое useRef

useRef — это хук, который возвращает изменяемый объект с единственным свойством .current. В отличие от useState, изменение .current не вызывает перерисовку компонента. Это делает useRef незаменимым в двух ситуациях: доступ к DOM-элементам и хранение значений между рендерами без лишних ре-рендеров.

// Базовый синтаксис
const myRef = useRef(initialValue)
// myRef.current === initialValue при первом рендере
// myRef.current сохраняется между рендерами
// изменение myRef.current НЕ вызывает ре-рендер

Доступ к DOM-элементам

Самый частый сценарий — программный фокус на input, измерение размеров элемента, управление видео/аудио плеером или интеграция с не-React библиотеками:

function AutoFocusInput() {
  const inputRef = useRef(null)

  useEffect(() => {
    // После монтирования компонента фокусируем инпут
    inputRef.current?.focus()
  }, [])

  // Передаём ref через атрибут ref= — React сам запишет DOM-узел в .current
  return <input ref={inputRef} placeholder="Автофокус при загрузке" />
}

React записывает ссылку на DOM-узел в ref.current после монтирования и обнуляет её до null при размонтировании.

Хранение мутабельных значений

useRef идеален для значений, которые нужно «помнить» между рендерами, но изменение которых не должно перерисовывать компонент:

function Stopwatch() {
  const [time, setTime] = useState(0)
  const intervalRef = useRef(null)  // хранит ID интервала

  const start = () => {
    // Сохраняем ID интервала — нужен для остановки
    intervalRef.current = setInterval(() => {
      setTime(t => t + 1)
    }, 1000)
  }

  const stop = () => {
    clearInterval(intervalRef.current)  // используем сохранённый ID
  }

  return (
    <div>
      <p>{time} секунд</p>
      <button onClick={start}>Старт</button>
      <button onClick={stop}>Стоп</button>
    </div>
  )
}

Если бы мы хранили intervalId в useState, каждый запуск таймера вызывал бы ненужный ре-рендер.

Сохранение предыдущего значения

Классический паттерн с useRef — доступ к предыдущему значению пропса или состояния:

function usePrevious(value) {
  const prevRef = useRef()

  useEffect(() => {
    // Обновляем ПОСЛЕ рендера — поэтому во время рендера
    // prevRef.current содержит предыдущее значение
    prevRef.current = value
  })

  return prevRef.current
}

function Counter() {
  const [count, setCount] = useState(0)
  const prevCount = usePrevious(count)

  return <p>Сейчас: {count}, было: {prevCount}</p>
}

forwardRef: передача ref в дочерний компонент

По умолчанию ref нельзя передать в функциональный компонент через пропс — React зарезервировал атрибут ref. Для этого используется forwardRef:

// Компонент-обёртка пробрасывает ref к нужному DOM-элементу
const FancyInput = forwardRef((props, ref) => (
  <div className="fancy-wrapper">
    <input ref={ref} {...props} />
  </div>
))

// Родительский компонент получает доступ к input внутри FancyInput
function Parent() {
  const inputRef = useRef(null)

  return (
    <>
      <FancyInput ref={inputRef} placeholder="Привет" />
      <button onClick={() => inputRef.current?.focus()}>
        Фокус
      </button>
    </>
  )
}

useRef vs useState

| | useState | useRef |

|---|---|---|

| Вызывает ре-рендер | Да | Нет |

| Читается в JSX | Да (напрямую) | Через .current |

| Использование | Данные для отображения | ID таймеров, DOM, предыдущие значения |

Хорошее правило: если значение нужно отобразить в UI — используй useState. Если нужно просто «запомнить» для логики — useRef.

Примеры

Реализация концепции useRef через замыкания: объект с .current, не вызывающий перерисовку, хранение ID интервала, предыдущее значение

// useRef — это просто объект с .current, который живёт всё время
// существования компонента и не вызывает ре-рендер при изменении.
// Реализуем эту концепцию вручную.

// --- Концепция useRef через замыкание ---

function createRef(initialValue) {
  // Просто объект! Вот и весь секрет useRef.
  return { current: initialValue }
}

// Симуляция компонента с "рендерами"
function createStopwatch() {
  let renderCount = 0

  // useRef: хранит ID интервала — изменение не вызывает ре-рендер
  const intervalRef = createRef(null)

  // useState: хранит время — изменение вызывает ре-рендер
  let time = 0
  const setTime = (newTime) => {
    time = newTime
    renderCount++
    console.log('  [ре-рендер #' + renderCount + '] time = ' + time + 'с')
  }

  const start = () => {
    console.log('Старт таймера...')
    // Сохраняем ID в ref — это НЕ вызывает ре-рендер
    intervalRef.current = setInterval(() => {
      setTime(time + 1)
    }, 100) // 100мс для быстрой демонстрации
    console.log('  intervalRef.current = ' + intervalRef.current + ' (без ре-рендера!)')
  }

  const stop = () => {
    clearInterval(intervalRef.current)
    console.log('Стоп. Итого ре-рендеров: ' + renderCount)
    console.log('Если бы ID хранился в state — было бы ' + (renderCount + 1) + ' ре-рендеров')
  }

  return { start, stop, getTime: () => time }
}

// --- Паттерн "предыдущее значение" ---

function createPreviousValue() {
  // ref не вызывает ре-рендер при обновлении
  const prevRef = createRef(undefined)

  // Обновляется ПОСЛЕ "рендера" (как useEffect)
  const afterRender = (currentValue) => {
    prevRef.current = currentValue
  }

  return { prevRef, afterRender }
}

// --- Демонстрация "фокуса" без реального DOM ---

function simulateDOMFocus() {
  // В реальном React:
  // const inputRef = useRef(null)
  // useEffect(() => { inputRef.current?.focus() }, [])
  // return <input ref={inputRef} />

  // Симуляция:
  const inputRef = createRef(null)

  // "Монтирование" — React записывает DOM-узел в .current
  const fakeInputElement = {
    focus() { console.log('  [DOM] input.focus() вызван — курсор установлен!') },
    value: '',
  }
  inputRef.current = fakeInputElement  // React делает это автоматически

  // Теперь можем вызвать focus программно
  console.log('После монтирования:')
  inputRef.current.focus()

  // "Размонтирование" — React обнуляет ref
  inputRef.current = null
  console.log('После размонтирования: inputRef.current = ' + inputRef.current)
}

// --- Запуск ---

console.log('=== Симуляция useRef для хранения intervalId ===')
const stopwatch = createStopwatch()
stopwatch.start()
setTimeout(() => {
  stopwatch.stop()

  console.log('\n=== Паттерн предыдущего значения ===')
  const prev = createPreviousValue()

  const values = [1, 5, 10, 3]
  values.forEach((val, i) => {
    console.log('Рендер ' + (i + 1) + ': current=' + val + ', previous=' + prev.prevRef.current)
    prev.afterRender(val)  // как useEffect — обновляется после рендера
  })

  console.log('\n=== Симуляция DOM-доступа через ref ===')
  simulateDOMFocus()
}, 500)

useRef: DOM-доступ и мутабельные значения

Что такое useRef

useRef — это хук, который возвращает изменяемый объект с единственным свойством .current. В отличие от useState, изменение .current не вызывает перерисовку компонента. Это делает useRef незаменимым в двух ситуациях: доступ к DOM-элементам и хранение значений между рендерами без лишних ре-рендеров.

// Базовый синтаксис
const myRef = useRef(initialValue)
// myRef.current === initialValue при первом рендере
// myRef.current сохраняется между рендерами
// изменение myRef.current НЕ вызывает ре-рендер

Доступ к DOM-элементам

Самый частый сценарий — программный фокус на input, измерение размеров элемента, управление видео/аудио плеером или интеграция с не-React библиотеками:

function AutoFocusInput() {
  const inputRef = useRef(null)

  useEffect(() => {
    // После монтирования компонента фокусируем инпут
    inputRef.current?.focus()
  }, [])

  // Передаём ref через атрибут ref= — React сам запишет DOM-узел в .current
  return <input ref={inputRef} placeholder="Автофокус при загрузке" />
}

React записывает ссылку на DOM-узел в ref.current после монтирования и обнуляет её до null при размонтировании.

Хранение мутабельных значений

useRef идеален для значений, которые нужно «помнить» между рендерами, но изменение которых не должно перерисовывать компонент:

function Stopwatch() {
  const [time, setTime] = useState(0)
  const intervalRef = useRef(null)  // хранит ID интервала

  const start = () => {
    // Сохраняем ID интервала — нужен для остановки
    intervalRef.current = setInterval(() => {
      setTime(t => t + 1)
    }, 1000)
  }

  const stop = () => {
    clearInterval(intervalRef.current)  // используем сохранённый ID
  }

  return (
    <div>
      <p>{time} секунд</p>
      <button onClick={start}>Старт</button>
      <button onClick={stop}>Стоп</button>
    </div>
  )
}

Если бы мы хранили intervalId в useState, каждый запуск таймера вызывал бы ненужный ре-рендер.

Сохранение предыдущего значения

Классический паттерн с useRef — доступ к предыдущему значению пропса или состояния:

function usePrevious(value) {
  const prevRef = useRef()

  useEffect(() => {
    // Обновляем ПОСЛЕ рендера — поэтому во время рендера
    // prevRef.current содержит предыдущее значение
    prevRef.current = value
  })

  return prevRef.current
}

function Counter() {
  const [count, setCount] = useState(0)
  const prevCount = usePrevious(count)

  return <p>Сейчас: {count}, было: {prevCount}</p>
}

forwardRef: передача ref в дочерний компонент

По умолчанию ref нельзя передать в функциональный компонент через пропс — React зарезервировал атрибут ref. Для этого используется forwardRef:

// Компонент-обёртка пробрасывает ref к нужному DOM-элементу
const FancyInput = forwardRef((props, ref) => (
  <div className="fancy-wrapper">
    <input ref={ref} {...props} />
  </div>
))

// Родительский компонент получает доступ к input внутри FancyInput
function Parent() {
  const inputRef = useRef(null)

  return (
    <>
      <FancyInput ref={inputRef} placeholder="Привет" />
      <button onClick={() => inputRef.current?.focus()}>
        Фокус
      </button>
    </>
  )
}

useRef vs useState

| | useState | useRef |

|---|---|---|

| Вызывает ре-рендер | Да | Нет |

| Читается в JSX | Да (напрямую) | Через .current |

| Использование | Данные для отображения | ID таймеров, DOM, предыдущие значения |

Хорошее правило: если значение нужно отобразить в UI — используй useState. Если нужно просто «запомнить» для логики — useRef.

Примеры

Реализация концепции useRef через замыкания: объект с .current, не вызывающий перерисовку, хранение ID интервала, предыдущее значение

// useRef — это просто объект с .current, который живёт всё время
// существования компонента и не вызывает ре-рендер при изменении.
// Реализуем эту концепцию вручную.

// --- Концепция useRef через замыкание ---

function createRef(initialValue) {
  // Просто объект! Вот и весь секрет useRef.
  return { current: initialValue }
}

// Симуляция компонента с "рендерами"
function createStopwatch() {
  let renderCount = 0

  // useRef: хранит ID интервала — изменение не вызывает ре-рендер
  const intervalRef = createRef(null)

  // useState: хранит время — изменение вызывает ре-рендер
  let time = 0
  const setTime = (newTime) => {
    time = newTime
    renderCount++
    console.log('  [ре-рендер #' + renderCount + '] time = ' + time + 'с')
  }

  const start = () => {
    console.log('Старт таймера...')
    // Сохраняем ID в ref — это НЕ вызывает ре-рендер
    intervalRef.current = setInterval(() => {
      setTime(time + 1)
    }, 100) // 100мс для быстрой демонстрации
    console.log('  intervalRef.current = ' + intervalRef.current + ' (без ре-рендера!)')
  }

  const stop = () => {
    clearInterval(intervalRef.current)
    console.log('Стоп. Итого ре-рендеров: ' + renderCount)
    console.log('Если бы ID хранился в state — было бы ' + (renderCount + 1) + ' ре-рендеров')
  }

  return { start, stop, getTime: () => time }
}

// --- Паттерн "предыдущее значение" ---

function createPreviousValue() {
  // ref не вызывает ре-рендер при обновлении
  const prevRef = createRef(undefined)

  // Обновляется ПОСЛЕ "рендера" (как useEffect)
  const afterRender = (currentValue) => {
    prevRef.current = currentValue
  }

  return { prevRef, afterRender }
}

// --- Демонстрация "фокуса" без реального DOM ---

function simulateDOMFocus() {
  // В реальном React:
  // const inputRef = useRef(null)
  // useEffect(() => { inputRef.current?.focus() }, [])
  // return <input ref={inputRef} />

  // Симуляция:
  const inputRef = createRef(null)

  // "Монтирование" — React записывает DOM-узел в .current
  const fakeInputElement = {
    focus() { console.log('  [DOM] input.focus() вызван — курсор установлен!') },
    value: '',
  }
  inputRef.current = fakeInputElement  // React делает это автоматически

  // Теперь можем вызвать focus программно
  console.log('После монтирования:')
  inputRef.current.focus()

  // "Размонтирование" — React обнуляет ref
  inputRef.current = null
  console.log('После размонтирования: inputRef.current = ' + inputRef.current)
}

// --- Запуск ---

console.log('=== Симуляция useRef для хранения intervalId ===')
const stopwatch = createStopwatch()
stopwatch.start()
setTimeout(() => {
  stopwatch.stop()

  console.log('\n=== Паттерн предыдущего значения ===')
  const prev = createPreviousValue()

  const values = [1, 5, 10, 3]
  values.forEach((val, i) => {
    console.log('Рендер ' + (i + 1) + ': current=' + val + ', previous=' + prev.prevRef.current)
    prev.afterRender(val)  // как useEffect — обновляется после рендера
  })

  console.log('\n=== Симуляция DOM-доступа через ref ===')
  simulateDOMFocus()
}, 500)

Задание

Создай компонент App с текстовым полем и кнопкой "Сфокусировать". При нажатии кнопки фокус программно устанавливается на поле ввода через useRef. Также добавь счётчик render-ов, хранимый в useRef (не в useState) — он не должен вызывать перерисовку.

Подсказка

useRef(null) создаёт ref. Привяжи его к input через ref={inputRef}. Для фокуса: inputRef.current.focus(). Обновляй счётчик через renderCount.current = renderCount.current + 1 — это не вызывает перерисовку, в отличие от setState.

Загружаем среду выполнения...
Загружаем AI-помощника...