Hack Frontend Community

Refs в React (useRef, createRef, forwardRef)

Что такое Refs?

Refs (ссылки) — это способ получить прямой доступ к DOM-элементам или React-компонентам из кода.

Refs используются когда нужно:

  • Управлять фокусом, выделением текста
  • Запускать анимации
  • Интегрироваться со сторонними библиотеками
  • Измерять размеры элементов

useRef

useRef — хук для создания рефов в функциональных компонентах.

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

import { useRef } from 'react';

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

  function handleClick() {
    // Получаем прямой доступ к input
    inputRef.current.focus();
  }

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={handleClick}>Focus Input</button>
    </div>
  );
}

Хранение изменяемого значения

useRef можно использовать для хранения любого значения, которое не вызывает перерисовку:

function Timer() {
  const [count, setCount] = useState(0);
  const intervalRef = useRef(null);

  function start() {
    if (intervalRef.current) return;
    
    intervalRef.current = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
  }

  function stop() {
    if (intervalRef.current) {
      clearInterval(intervalRef.current);
      intervalRef.current = null;
    }
  }

  useEffect(() => {
    return () => stop(); // Cleanup
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </div>
  );
}

useRef vs useState

// useState - вызывает перерисовку
const [value, setValue] = useState(0);

// useRef - НЕ вызывает перерисовку
const valueRef = useRef(0);
valueRef.current = 1; // Не вызовет render

createRef

createRef используется в классовых компонентах:

class TextInput extends React.Component {
  constructor(props) {
    super(props);
    this.inputRef = React.createRef();
  }

  handleClick = () => {
    this.inputRef.current.focus();
  };

  render() {
    return (
      <div>
        <input ref={this.inputRef} type="text" />
        <button onClick={this.handleClick}>Focus</button>
      </div>
    );
  }
}

Важно:

Не используйте createRef в функциональных компонентах! На каждом рендере будет создаваться новый реф. Используйте useRef.


forwardRef

forwardRef позволяет передать реф через компонент к его ребёнку.

Проблема

function CustomInput(props) {
  return <input {...props} />;
}

// Не работает! ref не пробрасывается
function Parent() {
  const inputRef = useRef(null);
  
  return <CustomInput ref={inputRef} />; // Ошибка!
}

Решение

const CustomInput = forwardRef((props, ref) => {
  return <input ref={ref} {...props} />;
});

function Parent() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <div>
      <CustomInput ref={inputRef} />
      <button onClick={handleClick}>Focus</button>
    </div>
  );
}

useImperativeHandle

useImperativeHandle позволяет настроить, что именно будет доступно родителю через реф.

Без useImperativeHandle

const CustomInput = forwardRef((props, ref) => {
  return <input ref={ref} />;
});

// Родитель получает доступ ко всему DOM input

С useImperativeHandle

import { forwardRef, useImperativeHandle, useRef } from 'react';

const CustomInput = forwardRef((props, ref) => {
  const inputRef = useRef(null);

  useImperativeHandle(ref, () => ({
    // Предоставляем только эти методы
    focus: () => {
      inputRef.current.focus();
    },
    scrollIntoView: () => {
      inputRef.current.scrollIntoView();
    }
    // value, blur и другие методы недоступны
  }));

  return <input ref={inputRef} {...props} />;
});

function Parent() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus(); // Работает
    // inputRef.current.value; // undefined!
  }

  return (
    <div>
      <CustomInput ref={inputRef} />
      <button onClick={handleClick}>Focus</button>
    </div>
  );
}

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

Автофокус при монтировании

function AutoFocusInput() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus();
  }, []);

  return <input ref={inputRef} />;
}

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

function MeasureComponent() {
  const divRef = useRef(null);
  const [dimensions, setDimensions] = useState({});

  useEffect(() => {
    if (divRef.current) {
      const { width, height } = divRef.current.getBoundingClientRect();
      setDimensions({ width, height });
    }
  }, []);

  return (
    <div>
      <div ref={divRef} style={{ padding: 20, backgroundColor: 'lightblue' }}>
        Measure me
      </div>
      <p>Width: {dimensions.width}px</p>
      <p>Height: {dimensions.height}px</p>
    </div>
  );
}

Интеграция со сторонней библиотекой

function VideoPlayer({ src }) {
  const videoRef = useRef(null);

  useEffect(() => {
    // Инициализация плеера со сторонней библиотекой
    const player = new ThirdPartyPlayer(videoRef.current);
    player.load(src);

    return () => {
      player.destroy();
    };
  }, [src]);

  return <video ref={videoRef} />;
}

Предыдущее значение

function usePrevious(value) {
  const ref = useRef();
  
  useEffect(() => {
    ref.current = value;
  }, [value]);
  
  return ref.current;
}

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

  return (
    <div>
      <p>Current: {count}</p>
      <p>Previous: {prevCount}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  );
}

Callback Refs

Альтернативный способ работы с рефами — callback функция:

function Component() {
  const [height, setHeight] = useState(0);

  const measuredRef = useCallback(node => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height);
    }
  }, []);

  return (
    <div>
      <div ref={measuredRef}>
        <p>This div's height is {height}px</p>
      </div>
    </div>
  );
}

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

Не используйте для того, что можно сделать декларативно

// Плохо
function Dialog() {
  const dialogRef = useRef(null);

  function open() {
    dialogRef.current.style.display = 'block';
  }

  function close() {
    dialogRef.current.style.display = 'none';
  }

  return <div ref={dialogRef}>Dialog</div>;
}

// Хорошо
function Dialog() {
  const [isOpen, setIsOpen] = useState(false);

  return isOpen ? <div>Dialog</div> : null;
}

Не храните данные, которые влияют на рендер

// Плохо
function Component() {
  const dataRef = useRef([]);

  function addItem(item) {
    dataRef.current.push(item); // Компонент не перерисуется!
  }

  return <div>{dataRef.current.length} items</div>;
}

// Хорошо
function Component() {
  const [data, setData] = useState([]);

  function addItem(item) {
    setData(prev => [...prev, item]); // Перерисовка произойдёт
  }

  return <div>{data.length} items</div>;
}

Типизация в TypeScript

import { useRef, forwardRef, useImperativeHandle } from 'react';

// useRef с DOM элементом
function Component() {
  const inputRef = useRef<HTMLInputElement>(null);

  function handleClick() {
    inputRef.current?.focus();
  }

  return <input ref={inputRef} />;
}

// forwardRef
interface Props {
  placeholder?: string;
}

const CustomInput = forwardRef<HTMLInputElement, Props>((props, ref) => {
  return <input ref={ref} {...props} />;
});

// useImperativeHandle
interface CustomInputHandle {
  focus: () => void;
  reset: () => void;
}

const CustomInput = forwardRef<CustomInputHandle, Props>((props, ref) => {
  const inputRef = useRef<HTMLInputElement>(null);

  useImperativeHandle(ref, () => ({
    focus() {
      inputRef.current?.focus();
    },
    reset() {
      if (inputRef.current) {
        inputRef.current.value = '';
      }
    }
  }));

  return <input ref={inputRef} {...props} />;
});

Частые ошибки

Доступ к ref.current до монтирования

// Неправильно
function Component() {
  const ref = useRef(null);
  console.log(ref.current); // null! Элемент ещё не создан

  return <div ref={ref}>Hello</div>;
}

// Правильно
function Component() {
  const ref = useRef(null);

  useEffect(() => {
    console.log(ref.current); // Элемент доступен
  }, []);

  return <div ref={ref}>Hello</div>;
}

Создание нового ref на каждом рендере

// Неправильно
function Component() {
  const ref = createRef(); // Новый ref на каждом рендере!
  
  return <div ref={ref}>Hello</div>;
}

// Правильно
function Component() {
  const ref = useRef(null); // Один и тот же ref
  
  return <div ref={ref}>Hello</div>;
}

Вывод

Refs в React:

  • useRef — для функциональных компонентов
  • createRef — для классовых компонентов
  • forwardRef — для проброса рефа через компонент
  • useImperativeHandle — для контроля доступа к рефу
  • Не вызывают перерисовку при изменении
  • Используйте когда декларативный подход невозможен
  • Избегайте для того, что можно сделать через state

На собеседовании:

Важно уметь:

  • Объяснить, что такое refs и когда их использовать
  • Показать разницу между useRef и useState
  • Объяснить, для чего нужны forwardRef и useImperativeHandle
  • Привести примеры правильного использования рефов
  • Рассказать, когда НЕ нужно использовать refs