← Назад к вопросам

Как useRef взаимодействует с DOM?

2.0 Middle🔥 211 комментариев
#JavaScript Core

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

useRef и взаимодействие с DOM в React

useRef — это React хук, который позволяет прямо обращаться к DOM-элементам и сохранять изменяемые значения, которые не вызывают перерендер при изменении. Это мост между React-абстракцией и браузерным DOM.

Основное назначение

useRef используется для:

  1. Прямого доступа к DOM (фокус, выбор текста, запуск анимации)
  2. Сохранения значений между рендерами без вызова перерендера
  3. Хранения идентификаторов таймеров для очистки
  4. Хранения предыдущих значений (previous props/state)

Как useRef взаимодействует с DOM

1. Создание ссылки на элемент

import { useRef } from 'react';

export function TextInput() {
  const inputRef = useRef(null);

  // inputRef.current — это реальный DOM-элемент
  // Изначально null, потом указывает на <input>

  const handleClick = () => {
    // Прямой доступ к DOM-элементу!
    inputRef.current?.focus();
    console.log(inputRef.current.value); // Значение инпута
  };

  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={handleClick}>Сфокусировать</button>
    </>
  );
}

Когда React рендерит <input ref={inputRef} ... />, он:

  1. Создаёт реальный DOM-элемент
  2. Заполняет inputRef.current этим элементом
  3. После этого ты можешь вызывать методы: .focus(), .blur(), .click()

2. Прямой доступ к DOM API

export function VideoPlayer() {
  const videoRef = useRef(null);

  const handlePlay = () => {
    // Вызов DOM методов напрямую
    videoRef.current?.play();
  };

  const handlePause = () => {
    videoRef.current?.pause();
  };

  const handleSetTime = (seconds) => {
    videoRef.current.currentTime = seconds;
  };

  return (
    <>
      <video ref={videoRef}>
        <source src="video.mp4" type="video/mp4" />
      </video>
      <button onClick={handlePlay}>Play</button>
      <button onClick={handlePause}>Pause</button>
      <button onClick={() => handleSetTime(10)}>Перейти на 10сек</button>
    </>
  );
}

Отличие useRef от useState

useState вызывает перерендер:

import { useState } from 'react';

export function Counter() {
  const [count, setCount] = useState(0);

  // При каждом вызове setCount() компонент перерендерится!
  return (
    <button onClick={() => setCount(count + 1)}>
      Счётчик: {count}
    </button>
  );
}

useRef НЕ вызывает перерендер:

import { useRef } from 'react';

export function StopWatch() {
  const countRef = useRef(0);

  const handleClick = () => {
    countRef.current++; // Изменили значение
    // Компонент НЕ перерендерится!
    console.log('Клики:', countRef.current);
  };

  return (
    <button onClick={handleClick}>
      Кликай (открой консоль)
    </button>
  );
}

Это полезно для:

  • Отслеживания количества рендеров
  • Хранения ID таймеров для очистки
  • Временных значений, которые не нужны в UI

Практические примеры

1. Фокус на инпут при ошибке

export function LoginForm() {
  const emailRef = useRef(null);
  const passwordRef = useRef(null);
  const [error, setError] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    
    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify({
          email: emailRef.current.value,
          password: passwordRef.current.value
        })
      });

      if (!response.ok) {
        setError('Неверный пароль');
        // Сфокусируем на пароль для удобства пользователя
        passwordRef.current?.focus();
      }
    } catch (err) {
      setError(err.message);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input ref={emailRef} type="email" placeholder="Email" />
      <input ref={passwordRef} type="password" placeholder="Пароль" />
      {error && <p style={{ color: 'red' }}>{error}</p>}
      <button type="submit">Вход</button>
    </form>
  );
}

2. Интеграция с внешней библиотекой (jQuery плагин, D3, Leaflet)

import { useRef, useEffect } from 'react';
import * as d3 from 'd3';

export function Chart({ data }) {
  const svgRef = useRef(null);

  useEffect(() => {
    if (!svgRef.current) return;

    // Используем D3 с реальным DOM-элементом
    const svg = d3.select(svgRef.current);

    svg
      .selectAll('circle')
      .data(data)
      .enter()
      .append('circle')
      .attr('cx', (d, i) => i * 30)
      .attr('cy', (d) => d * 10)
      .attr('r', 5)
      .attr('fill', 'blue');
  }, [data]);

  return <svg ref={svgRef} width="800" height="400" />;
}

3. Текстовое поле с автосохранением (без вызова useState при каждом символе)

import { useRef, useEffect } from 'react';

export function AutoSaveTextarea() {
  const textRef = useRef(null);
  const saveTimeoutRef = useRef(null);

  const handleChange = () => {
    // Очищаем предыдущий таймер
    clearTimeout(saveTimeoutRef.current);

    // Устанавливаем новый таймер для сохранения
    saveTimeoutRef.current = setTimeout(() => {
      const text = textRef.current?.value;
      console.log('Автосохранение:', text);
      // Отправляем на сервер
    }, 1000);
  };

  useEffect(() => {
    return () => {
      // Очистка при размонтировании
      clearTimeout(saveTimeoutRef.current);
    };
  }, []);

  return (
    <textarea
      ref={textRef}
      onChange={handleChange}
      placeholder="Напиши текст (автосохранится через 1 сек)"
    />
  );
}

4. Выделение текста в инпуте

export function SelectableInput() {
  const inputRef = useRef(null);

  const selectAll = () => {
    inputRef.current?.select();
  };

  return (
    <>
      <input
        ref={inputRef}
        type="text"
        defaultValue="Текст для выделения"
      />
      <button onClick={selectAll}>Выделить всё</button>
    </>
  );
}

5. Хранение предыдущего значения State

import { useRef, useEffect, useState } from 'react';

export function PreviousValue() {
  const [count, setCount] = useState(0);
  const prevCountRef = useRef(null);

  useEffect(() => {
    // После рендера обновляем prevCountRef
    prevCountRef.current = count;
  }, [count]);

  return (
    <>
      <p>Текущее: {count}, Предыдущее: {prevCountRef.current}</p>
      <button onClick={() => setCount(count + 1)}>Увеличить</button>
    </>
  );
}

Жизненный цикл useRef

import { useRef, useEffect } from 'react';

export function LifecycleExample() {
  const ref = useRef(null);

  useEffect(() => {
    console.log('Mount: ref.current =', ref.current);
    // На момент первого useEffect ref.current уже указывает на DOM!

    return () => {
      console.log('Unmount');
      // ref.current всё ещё доступен для очистки
    };
  }, []);

  return <div ref={ref}>Содержимое</div>;
}

// Вывод:
// Mount: ref.current = <div>Содержимое</div>
// (при размонтировании) Unmount

Когда НЕ использовать useRef

// ❌ Плохо: используем useRef для значений, влияющих на UI
const countRef = useRef(0);
return (
  <button onClick={() => countRef.current++}>
    {countRef.current} <!-- Не обновится! -->
  </button>
);

// ✅ Хорошо: используем useState
const [count, setCount] = useState(0);
return (
  <button onClick={() => setCount(count + 1)}>
    {count}
  </button>
);

Получение информации из DOM

export function GetElementInfo() {
  const elementRef = useRef(null);

  const getInfo = () => {
    const elem = elementRef.current;
    console.log('offsetWidth:', elem?.offsetWidth); // Ширина
    console.log('offsetHeight:', elem?.offsetHeight); // Высота
    console.log('scrollHeight:', elem?.scrollHeight); // Высота с прокруткой
    console.log('getBoundingClientRect:', elem?.getBoundingClientRect());
  };

  return (
    <>
      <div ref={elementRef} style={{ width: '200px', height: '100px' }}>
        Элемент
      </div>
      <button onClick={getInfo}>Получить информацию</button>
    </>
  );
}

Типизация useRef в TypeScript

import { useRef } from 'react';

export function TypedRef() {
  // Тип элемента
  const inputRef = useRef<HTMLInputElement>(null);
  
  const handleFocus = () => {
    inputRef.current?.focus();
    const value = inputRef.current?.value; // Type: string | undefined
  };

  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={handleFocus}>Сфокусировать</button>
    </>
  );
}

// Для хранения значений (не элементов)
export function TypedValue() {
  const timeoutRef = useRef<NodeJS.Timeout | null>(null);
  
  const startTimer = () => {
    timeoutRef.current = setTimeout(() => {
      console.log('Время вышло');
    }, 1000);
  };

  return <button onClick={startTimer}>Запустить таймер</button>;
}

Внутренний механизм: как React работает с refs

// Когда ты пишешь:
const ref = useRef(null);
return <div ref={ref} />;

// React внутренне:
// 1. Создаёт реальный DOM-элемент: const domElement = document.createElement('div');
// 2. Присваивает: ref.current = domElement;
// 3. Монтирует в DOM: parent.appendChild(domElement);

// Теперь ref.current указывает на реальный DOM-элемент в браузере!

Заключение

useRef и DOM:

  • useRef.current = реальный DOM-элемент после монтирования
  • Позволяет вызывать методы: .focus(), .play(), .select()
  • Позволяет читать свойства: .value, .offsetWidth, .classList
  • НЕ вызывает перерендер при изменении
  • Идеален для интеграции с внешними библиотеками
  • Следует использовать только когда нет альтернативы через React (props, state, callback)