useRef — это хук, который возвращает изменяемый объект с единственным свойством .current. В отличие от useState, изменение .current не вызывает перерисовку компонента. Это делает useRef незаменимым в двух ситуациях: доступ к DOM-элементам и хранение значений между рендерами без лишних ре-рендеров.
// Базовый синтаксис
const myRef = useRef(initialValue)
// myRef.current === initialValue при первом рендере
// myRef.current сохраняется между рендерами
// изменение myRef.current НЕ вызывает ре-рендерСамый частый сценарий — программный фокус на 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>
}По умолчанию 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>
</>
)
}| | 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 — это хук, который возвращает изменяемый объект с единственным свойством .current. В отличие от useState, изменение .current не вызывает перерисовку компонента. Это делает useRef незаменимым в двух ситуациях: доступ к DOM-элементам и хранение значений между рендерами без лишних ре-рендеров.
// Базовый синтаксис
const myRef = useRef(initialValue)
// myRef.current === initialValue при первом рендере
// myRef.current сохраняется между рендерами
// изменение myRef.current НЕ вызывает ре-рендерСамый частый сценарий — программный фокус на 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>
}По умолчанию 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>
</>
)
}| | 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.