Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Зачем нужен хук useRef
Это один из самых непонятных хуков в React, потому что он ломает основной принцип функциональных компонентов. После 10+ лет работы с React скажу: useRef нужен редко, но когда он нужен, он критичен.
Что такое useRef
useRef — это хук, который возвращает объект с неизменяемой ссылкой на значение. Ключевая особенность: обновление ref НЕ вызывает повторный рендер компонента.
const ref = useRef(initialValue);
// ref.current содержит значение
Основные различия между useState и useRef
| Характеристика | useState | useRef |
|---|---|---|
| Вызывает ре-рендер | Да | Нет |
| Мутабельность | Иммутабельно (новое значение) | Мутабельно (изменение 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. Ключевые правила:
-
Используй useRef для:
- Доступ к DOM элементам (focus, play, select)
- Сохранение ID таймеров/интервалов
- Значения, которые не должны вызывать ре-рендер
-
Не используй useRef для:
- UI состояния (используй useState)
- Значений, которые должны быть в UI (используй useState)
-
Помни: Изменение ref.current НЕ вызывает ре-рендер, но значение сохраняется между рендерами.