Расскажите про хук - useRef, как он работает, где применяется и для чего?

👨‍💻 Frontend Developer 🟡 Часто попадается 🎚️ Средний
#React #Hooks

Краткий ответ

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

Ключевые особенности useRef:

  • Изменение .current не вызывает повторный рендер
  • Значение сохраняется между рендерами
  • Каждый экземпляр компонента получает свой независимый ref

Основные применения:

  1. Доступ к DOM-элементам
  2. Хранение предыдущих значений состояния
  3. Сохранение мутируемых значений без вызова повторного рендера

Пример доступа к DOM-элементу:

function TextInputWithFocusButton() {
  const inputRef = useRef(null);
  
  const focusInput = () => {
    inputRef.current.focus();
  };
  
  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>Фокус на поле ввода</button>
    </>
  );
}

Полный ответ

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

Как работает useRef?

useRef возвращает простой JavaScript-объект с одним свойством current:

// Создание ref с начальным значением null
const myRef = useRef(null);
console.log(myRef); // { current: null }
 
// Можно установить начальное значение
const countRef = useRef(0);
console.log(countRef); // { current: 0 }
 
// Изменение значения не вызывает ререндер
countRef.current = countRef.current + 1;

В отличие от состояния, изменение current происходит синхронно и не вызывает перерисовку компонента.

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

Самое распространенное применение useRef — получение прямого доступа к DOM-узлам:

function AutoFocusInput() {
  const inputRef = useRef(null);
  
  // После монтирования компонента фокусируемся на инпуте
  useEffect(() => {
    inputRef.current.focus();
  }, []);
  
  return <input ref={inputRef} />;
}

Управление видео и аудио элементами:

function VideoPlayer({ src }) {
  const videoRef = useRef(null);
  
  const handlePlay = () => {
    videoRef.current.play();
  };
  
  const handlePause = () => {
    videoRef.current.pause();
  };
  
  return (
    <div>
      <video ref={videoRef} src={src} />
      <button onClick={handlePlay}>Play</button>
      <button onClick={handlePause}>Pause</button>
    </div>
  );
}

Измерение размеров элемента:

function MeasureExample() {
  const divRef = useRef(null);
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
  
  useEffect(() => {
    if (divRef.current) {
      setDimensions({
        width: divRef.current.offsetWidth,
        height: divRef.current.offsetHeight
      });
    }
  }, []);
  
  return (
    <>
      <div ref={divRef} style={{ width: '100%', height: '100px', border: '1px solid black' }}>
        Измеряемый элемент
      </div>
      <p>Ширина: {dimensions.width}px, Высота: {dimensions.height}px</p>
    </>
  );
}

2. Хранение предыдущих значений состояния

Отслеживание предыдущих значений состояния, которые недоступны напрямую:

function CounterWithPrevious() {
  const [count, setCount] = useState(0);
  const prevCountRef = useRef();
  
  useEffect(() => {
    // Сохраняем текущее значение count после рендера
    prevCountRef.current = count;
  });
  
  const prevCount = prevCountRef.current;
  
  return (
    <div>
      <h1>Сейчас: {count}, до этого: {prevCount !== undefined ? prevCount : 'Нет предыдущего значения'}</h1>
      <button onClick={() => setCount(count + 1)}>Увеличить</button>
    </div>
  );
}

3. Сохранение мутируемых значений

Когда нужно хранить значение, которое не влияет на UI:

function IntervalExample() {
  const [count, setCount] = useState(0);
  const intervalIdRef = useRef(null);
  
  const startCounter = () => {
    if (intervalIdRef.current !== null) return;
    
    intervalIdRef.current = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
  };
  
  const stopCounter = () => {
    clearInterval(intervalIdRef.current);
    intervalIdRef.current = null;
  };
  
  // Очистка при размонтировании
  useEffect(() => {
    return () => {
      if (intervalIdRef.current !== null) {
        clearInterval(intervalIdRef.current);
      }
    };
  }, []);
  
  return (
    <div>
      <h1>Счетчик: {count}</h1>
      <button onClick={startCounter}>Старт</button>
      <button onClick={stopCounter}>Стоп</button>
    </div>
  );
}

4. Кэширование вычислений без перерендеров

Сохранение результатов вычислений, которые не должны приводить к перерисовке:

function ExpensiveComponent({ data }) {
  const cachedDataRef = useRef(null);
  
  if (cachedDataRef.current === null || cachedDataRef.current.originalData !== data) {
    // Выполнить дорогостоящие вычисления только при изменении data
    const processedResult = expensiveCalculation(data);
    
    cachedDataRef.current = {
      originalData: data,
      processedResult
    };
  }
  
  // Используем кэшированный результат
  return <div>{cachedDataRef.current.processedResult}</div>;
}

5. Создание пользовательских хуков с useRef

Пример хука для отслеживания, был ли компонент смонтирован:

function useIsMounted() {
  const isMountedRef = useRef(false);
  
  useEffect(() => {
    isMountedRef.current = true;
    
    return () => {
      isMountedRef.current = false;
    };
  }, []);
  
  return isMountedRef;
}
 
// Использование
function AsyncComponent() {
  const [data, setData] = useState(null);
  const isMountedRef = useIsMounted();
  
  useEffect(() => {
    fetchData().then(result => {
      // Проверяем, смонтирован ли компонент перед обновлением состояния
      if (isMountedRef.current) {
        setData(result);
      }
    });
  }, []);
  
  return <div>{data ? JSON.stringify(data) : 'Загрузка...'}</div>;
}

Сравнение с другими способами хранения данных

useRef vs useState

// С useState - перерисовка при каждом изменении
function CounterWithState() {
  const [count, setCount] = useState(0);
  
  // Перерисовывает компонент
  const increment = () => setCount(count + 1);
  
  console.log("Рендер компонента с useState");
  
  return (
    <div>
      <p>Счетчик (useState): {count}</p>
      <button onClick={increment}>Увеличить</button>
    </div>
  );
}
 
// С useRef - без перерисовок
function CounterWithRef() {
  const countRef = useRef(0);
  const [, forceUpdate] = useState({});
  
  // Не вызывает перерисовку
  const increment = () => {
    countRef.current += 1;
  };
  
  // Для отображения актуального значения нужно вызвать ререндер
  const incrementAndUpdate = () => {
    increment();
    forceUpdate({});
  };
  
  console.log("Рендер компонента с useRef");
  
  return (
    <div>
      <p>Счетчик (useRef): {countRef.current}</p>
      <button onClick={increment}>Увеличить (без обновления UI)</button>
      <button onClick={incrementAndUpdate}>Увеличить и обновить UI</button>
    </div>
  );
}

useRef vs createRef

function CompareRefs() {
  // Создается заново при каждом рендере
  const createRefExample = React.createRef();
  
  // Сохраняется между рендерами
  const useRefExample = useRef();
  
  // Демонстрация различий
  const [, forceRender] = useState({});
  
  useEffect(() => {
    console.log("После монтирования:");
    console.log("createRef текущее значение:", createRefExample.current);
    console.log("useRef текущее значение:", useRefExample.current);
    
    // Устанавливаем значения
    createRefExample.current = "Значение createRef";
    useRefExample.current = "Значение useRef";
    
    console.log("После установки:");
    console.log("createRef значение:", createRefExample.current);
    console.log("useRef значение:", useRefExample.current);
  }, []);
  
  const handleClick = () => {
    // Перерисовываем компонент
    forceRender({});
    
    // После перерисовки
    console.log("После перерисовки:");
    console.log("createRef значение:", createRefExample.current); // null
    console.log("useRef значение:", useRefExample.current); // "Значение useRef"
  };
  
  return <button onClick={handleClick}>Перерисовать</button>;
}

Распространенные ошибки

1. Попытка наблюдения за изменениями ref

// ❌ Неправильно: useEffect не отслеживает изменения ref.current
function WrongWayToWatchRef() {
  const countRef = useRef(0);
  
  useEffect(() => {
    console.log("Значение изменилось:", countRef.current);
  }, [countRef.current]); // Это не сработает как ожидается
  
  return (
    <button onClick={() => { countRef.current += 1; }}>
      Увеличить (не вызовет useEffect)
    </button>
  );
}
 
// ✅ Правильно: использовать state для отслеживаемых значений
function CorrectWayToWatchChanges() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    console.log("Значение изменилось:", count);
  }, [count]);
  
  return (
    <button onClick={() => setCount(count + 1)}>
      Увеличить (вызовет useEffect)
    </button>
  );
}

2. Инициализация ref условно

// ❌ Неправильно: создание ref внутри условия
function ConditionalRef({ shouldRender }) {
  let inputRef;
  
  if (shouldRender) {
    inputRef = useRef(null);
  }
  
  // При изменении shouldRender возникнет ошибка
  return shouldRender ? <input ref={inputRef} /> : null;
}
 
// ✅ Правильно: всегда создавать ref на верхнем уровне
function CorrectConditionalRendering({ shouldRender }) {
  const inputRef = useRef(null);
  
  return shouldRender ? <input ref={inputRef} /> : null;
}

3. Попытка использовать ref до присвоения

// ❌ Неправильно: обращение к ref до его присвоения
function TooEarlyAccess() {
  const inputRef = useRef(null);
  
  // Этот код выполнится до того, как ref получит значение
  console.log(inputRef.current.value); // Ошибка: Cannot read property 'value' of null
  
  return <input ref={inputRef} defaultValue="Начальное значение" />;
}
 
// ✅ Правильно: использовать useEffect для доступа после рендера
function CorrectAccess() {
  const inputRef = useRef(null);
  
  useEffect(() => {
    // Этот код выполнится после того, как DOM будет готов
    console.log(inputRef.current.value);
  }, []);
  
  return <input ref={inputRef} defaultValue="Начальное значение" />;
}

Резюме

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

  • Доступа к DOM-элементам и их методам (focus(), play(), и т.д.)
  • Хранения предыдущих значений состояния
  • Сохранения значений между рендерами без вызова перерисовки
  • Хранения таймеров, интервалов и других ссылок, не относящихся к UI
  • Создания пользовательских хуков для отслеживания состояния компонента

Ключевые особенности:

  • Изменение .current не вызывает повторного рендера
  • Значение сохраняется на протяжении всего жизненного цикла компонента
  • Отличается от useState отсутствием автоматической перерисовки
  • Отличается от createRef сохранением значения между рендерами

Понимание useRef и правильное его применение — важная часть оптимизации производительности и взаимодействия с DOM в React-приложениях. 🚀


Хотите больше статей для подготовки к собеседованиям? Подписывайтесь на EasyAdvice, добавляйте сайт в закладки и совершенствуйтесь каждый день 💪