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

Зачем нужен хук useRef?

2.0 Middle🔥 171 комментариев
#React

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

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

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

Зачем нужен хук useRef

Это один из самых непонятных хуков в React, потому что он ломает основной принцип функциональных компонентов. После 10+ лет работы с React скажу: useRef нужен редко, но когда он нужен, он критичен.

Что такое useRef

useRef — это хук, который возвращает объект с неизменяемой ссылкой на значение. Ключевая особенность: обновление ref НЕ вызывает повторный рендер компонента.

const ref = useRef(initialValue);
// ref.current содержит значение

Основные различия между useState и useRef

ХарактеристикаuseStateuseRef
Вызывает ре-рендерДаНет
МутабельностьИммутабельно (новое значение)Мутабельно (изменение current)
Сохранение между рендерамиДаДа
Когда использоватьДля UI состоянияДля значений, которые UI не нужны
// ❌ Неправильно: useRef когда нужен ре-рендер
function BadCounter() {
  const count = useRef(0);
  
  return (
    <button onClick={() => {
      count.current++;
      // UI не обновится!
    }}>
      {count.current} {/* всегда 0 */}
    </button>
  );
}

// ✅ Правильно: useState для UI состояния
function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <button onClick={() => setCount(c => c + 1)}>
      {count} {/* обновляется */}
    </button>
  );
}

Случай 1: Доступ к DOM элементам

Самый частый использованный случай — получить прямой доступ к DOM:

function TextInput() {
  const inputRef = useRef(null);
  
  const focusInput = () => {
    inputRef.current.focus(); // прямой доступ к DOM API
  };
  
  const selectAll = () => {
    inputRef.current.select(); // нельзя сделать через props
  };
  
  return (
    <>
      <input ref={inputRef} />
      <button onClick={focusInput}>Сфокусироваться</button>
      <button onClick={selectAll}>Выделить всё</button>
    </>
  );
}

Реальный пример: медиаплеер

function VideoPlayer({ videoUrl }: { videoUrl: string }) {
  const videoRef = useRef<HTMLVideoElement>(null);
  const [isPlaying, setIsPlaying] = useState(false);
  
  const play = () => {
    videoRef.current?.play();
    setIsPlaying(true);
  };
  
  const pause = () => {
    videoRef.current?.pause();
    setIsPlaying(false);
  };
  
  const seek = (time: number) => {
    if (videoRef.current) {
      videoRef.current.currentTime = time;
    }
  };
  
  return (
    <>
      <video ref={videoRef} src={videoUrl} />
      <button onClick={play}>Play</button>
      <button onClick={pause}>Pause</button>
      <button onClick={() => seek(10)}>Skip 10s</button>
    </>
  );
}

Случай 2: Хранение переменной, которая не влияет на рендер

Есть значения, которые нужно сохранять между рендерами, но не нужно выводить в UI:

function Timer() {
  const [count, setCount] = useState(0);
  const intervalRef = useRef(null); // сохраняем ID интервала
  
  const start = () => {
    // Нельзя просто присваивать, потому что состояние очистится
    intervalRef.current = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
  };
  
  const stop = () => {
    clearInterval(intervalRef.current); // достаём ID из ref
  };
  
  useEffect(() => {
    return () => clearInterval(intervalRef.current); // cleanup
  }, []);
  
  return (
    <>
      <p>{count}</p>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </>
  );
}

Случай 3: Отслеживание предыдущего значения

function ValueComparison({ value }) {
  const prevValueRef = useRef();
  
  useEffect(() => {
    // сохраняем текущее значение для следующего рендера
    prevValueRef.current = value;
  }, [value]);
  
  return (
    <p>
      Сейчас: {value}
      {prevValueRef.current !== undefined && (
        <span> (было: {prevValueRef.current})</span>
      )}
    </p>
  );
}

// Или как кастомный хук
function usePrevious(value) {
  const ref = useRef();
  
  useEffect(() => {
    ref.current = value;
  }, [value]);
  
  return ref.current;
}

function Component({ value }) {
  const prevValue = usePrevious(value);
  return <p>Было {prevValue}, стало {value}</p>;
}

Случай 4: Оптимизация производительности

Иногда нужно передать значение в обработчик события, но не хочется создавать новую функцию каждый раз:

function SearchInput() {
  const [query, setQuery] = useState("");
  const queryRef = useRef(query);
  
  useEffect(() => {
    queryRef.current = query; // обновляем ref
  }, [query]);
  
  // Обработчик не меняется, поэтому нет ре-креирования функции
  const handleSubmit = useCallback(() => {
    // используем актуальное значение query из ref
    console.log(queryRef.current);
  }, []);
  
  return (
    <>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <button onClick={handleSubmit}>Search</button>
    </>
  );
}

Случай 5: Работа с библиотеками

import CodeMirror from "@uiw/react-codemirror";

function Editor() {
  const editorRef = useRef<any>(null);
  
  const formatCode = () => {
    // Библиотека требует прямой доступ к инстансу
    editorRef.current?.editor?.formatCode();
  };
  
  const getCode = () => {
    // Получить значение через API библиотеки
    return editorRef.current?.editor?.getValue();
  };
  
  return (
    <>
      <CodeMirror ref={editorRef} value="const x = 1;" />
      <button onClick={formatCode}>Format</button>
      <button onClick={() => console.log(getCode())}>Get Code</button>
    </>
  );
}

Случай 6: Imperative API

Иногда нужно дать родителю методы для управления компонентом:

export interface ModalHandle {
  open: () => void;
  close: () => void;
}

const Modal = forwardRef<ModalHandle>((props, ref) => {
  const [isOpen, setIsOpen] = useState(false);
  
  useImperativeHandle(ref, () => ({
    open: () => setIsOpen(true),
    close: () => setIsOpen(false),
  }));
  
  return isOpen ? <div>Modal content</div> : null;
});

// Использование
function App() {
  const modalRef = useRef<ModalHandle>(null);
  
  return (
    <>
      <button onClick={() => modalRef.current?.open()}>Open Modal</button>
      <Modal ref={modalRef} />
    </>
  );
}

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

// ❌ Плохо: использовать ref для значений, которые нужны в UI
function Counter() {
  const countRef = useRef(0);
  return (
    <button onClick={() => countRef.current++}>
      {countRef.current} {/* никогда не обновится */}
    </button>
  );
}

// ❌ Плохо: использовать ref как замену useState
function Form() {
  const formRef = useRef({ name: "", email: "" });
  // Лучше использовать useState
}

// ❌ Плохо: передавать ref в условном рендере
function Bad() {
  const ref = useRef(null);
  
  return (
    <>
      {condition && <div ref={ref}>Content</div>}
      <button onClick={() => ref.current?.focus()}>Focus</button>
    </>
  );
}

useCallback vs useRef

Этот вопрос часто ставит разработчиков в тупик:

// useRef подход
function Component({ callback }) {
  const callbackRef = useRef(callback);
  
  useEffect(() => {
    callbackRef.current = callback;
  }, [callback]);
  
  const handleClick = useCallback(() => {
    callbackRef.current(); // всегда актуальный callback
  }, []);
  
  return <Child onClick={handleClick} />;
}

// useCallback подход (обычно проще)
function Component({ callback }) {
  const handleClick = useCallback(() => {
    callback();
  }, [callback]);
  
  return <Child onClick={handleClick} />;
}

Производительность

function Benchmark() {
  const [count, setCount] = useState(0);
  const refCount = useRef(0);
  
  return (
    <>
      {/* обновление state вызывает ре-рендер */}
      <button onClick={() => setCount(c => c + 1)}>useState: {count}</button>
      
      {/* обновление ref НЕ вызывает ре-рендер */}
      <button
        onClick={() => {
          refCount.current++;
          // UI не обновится, но значение сохранится
        }}
      >
        useRef: {refCount.current}
      </button>
    </>
  );
}

Заключение

useRef — это "лазейка" из React-парадигмы для случаев, когда нужен прямой доступ к DOM или значения, которые не влияют на UI. Ключевые правила:

  1. Используй useRef для:

    • Доступ к DOM элементам (focus, play, select)
    • Сохранение ID таймеров/интервалов
    • Значения, которые не должны вызывать ре-рендер
  2. Не используй useRef для:

    • UI состояния (используй useState)
    • Значений, которые должны быть в UI (используй useState)
  3. Помни: Изменение ref.current НЕ вызывает ре-рендер, но значение сохраняется между рендерами.

Зачем нужен хук useRef? | PrepBro