React Front-end Инженер

React Front-end Инженер

Роадмап навыков для прокачки

Что такое ref в React

ReactComponentsRefs (ссылки на элементы)

Основная идея

Ref (reference) — это механизм React для получения прямого доступа к DOM-элементу или экземпляру компонента, минуя стандартный декларативный подход через props и state.

Ключевые аспекты

  • Прямой доступ к DOM — позволяет работать с реальным DOM-элементом напрямую
  • Сохранение значений между рендерами — ref.current не вызывает перерисовку при изменении
  • Создание — через useRef() в функциональных компонентах или createRef() в классовых
  • Присваивание — через атрибут ref={myRef} на JSX-элементе
  • Доступ — через свойство ref.current

Плюсы

  • Позволяет интегрироваться с DOM API (фокус, измерение размеров)
  • Не вызывает лишних рендеров при изменении
  • Необходим для работы со сторонними библиотеками

Минусы

  • Нарушает декларативный подход React
  • Может привести к трудноотлаживаемому коду
  • Не работает напрямую с функциональными компонентами без forwardRef

Частые ошибки на собеседованиях

  • Путают ref с state — state вызывает рендер, ref нет
  • Пытаются использовать ref вместо state для отображаемых данных
  • Забывают про forwardRef при передаче ref дочернему компоненту
  • Обращаются к ref.current до монтирования компонента

Введение и проблематика

React построен на декларативном подходе: мы описываем, что должно отображаться, а не как это сделать. Однако иногда требуется выйти за рамки этого подхода и напрямую взаимодействовать с DOM.

Ref — это "аварийный люк" для императивного взаимодействия с DOM, когда декларативный подход React недостаточен.

Когда нужен прямой доступ к DOM?

  • Управление фокусом (focus/blur)
  • Выделение текста
  • Запуск анимаций
  • Интеграция со сторонними DOM-библиотеками
  • Измерение размеров и позиции элементов

Базовая теория

Что такое ref?

Ref (reference) — это объект с единственным свойством current, который:

  • Создаётся один раз и сохраняется между рендерами
  • Может хранить любое изменяемое значение
  • При изменении current не вызывает перерисовку компонента
ts
// Структура ref-объекта
interface RefObject<T> {
  current: T | null;
}

Создание ref

Code Example 1: В чём разница между useRef и createRef? Когда какой использовать?

tsx
import { useRef } from 'react';
 
function MyComponent() {
  // Создаём ref с начальным значением null
  const inputRef = useRef<HTMLInputElement>(null);
 
  return <input ref={inputRef} />;
}

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

Управление фокусом

Code Example 2: Что делает passwordRef.current?.focus()? Когда ref.current становится доступным?

tsx
import { useRef } from 'react';
 
function LoginForm() {
  const emailRef = useRef<HTMLInputElement>(null);
  const passwordRef = useRef<HTMLInputElement>(null);
 
  const handleEmailSubmit = () => {
    // Переводим фокус на поле пароля
    passwordRef.current?.focus();
  };
 
  return (
    <form>
      <input
        ref={emailRef}
        type="email"
        placeholder="Email"
        onKeyDown={(e) => e.key === 'Enter' && handleEmailSubmit()}
      />
      <input
        ref={passwordRef}
        type="password"
        placeholder="Пароль"
      />
    </form>
  );
}

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

Code Example 3: Почему для хранения предыдущего значения используется ref, а не state? Что происходит при изменении ref.current?

tsx
import { useRef, useEffect, useState } from 'react';
 
function Counter() {
  const [count, setCount] = useState(0);
  const prevCountRef = useRef<number>(0);
 
  useEffect(() => {
    // Сохраняем предыдущее значение после каждого рендера
    prevCountRef.current = count;
  });
 
  return (
    <div>
      <p>Текущее: {count}</p>
      <p>Предыдущее: {prevCountRef.current}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
    </div>
  );
}

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

Code Example 4: Зачем здесь нужны два ref? Как происходит интеграция с внешней библиотекой?

tsx
import { useRef, useEffect } from 'react';
import Chart from 'chart.js';
 
function ChartComponent({ data }) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const chartRef = useRef<Chart | null>(null);
 
  useEffect(() => {
    if (canvasRef.current) {
      // Создаём экземпляр Chart.js
      chartRef.current = new Chart(canvasRef.current, {
        type: 'line',
        data: data
      });
    }
 
    // Очистка при размонтировании
    return () => {
      chartRef.current?.destroy();
    };
  }, [data]);
 
  return <canvas ref={canvasRef} />;
}

Ref vs State

Характеристикаrefstate
Вызывает рендер при изменении❌ Нет✅ Да
Изменяемый (mutable)✅ Да❌ Нет
Доступ к значениюСинхронныйПосле рендера
ИспользованиеDOM, таймеры, предыдущие значенияОтображаемые данные
⚠️

Никогда не используйте ref для данных, которые должны отображаться в UI. Изменение ref.current не вызовет перерисовку!


Пограничные кейсы

Ref на функциональный компонент

По умолчанию нельзя передать ref функциональному компоненту — нужен forwardRef:

Code Example 5: Почему Version A не работает? Что делает forwardRef?

tsx
import { forwardRef, useRef } from 'react';
 
// ❌ Неправильно — ref не будет работать
function MyInput(props) {
  return <input {...props} />;
}
 
// ✅ Правильно — используем forwardRef
const MyInput = forwardRef<HTMLInputElement, Props>((props, ref) => {
  return <input ref={ref} {...props} />;
});
 
// Использование
function Parent() {
  const inputRef = useRef<HTMLInputElement>(null);
  return <MyInput ref={inputRef} />;
}

Callback ref

Code Example 6: Чем callback ref отличается от объекта ref? Когда вызывается эта функция?

Вместо объекта ref можно передать функцию:

tsx
function MeasureExample() {
  const measureRef = (node: HTMLDivElement | null) => {
    if (node !== null) {
      console.log('Размеры:', node.getBoundingClientRect());
    }
  };
 
  return <div ref={measureRef}>Измеряемый элемент</div>;
}

Плюсы и минусы

Плюсы

  • Прямой доступ к DOM когда это необходимо
  • Не вызывает лишних рендеров
  • Позволяет хранить мутабельные значения
  • Необходим для интеграции с внешними библиотеками

Минусы

  • Нарушает декларативную парадигму React
  • Может привести к трудноотлаживаемому коду
  • Требует понимания жизненного цикла компонента
  • Значение доступно только после монтирования

Вопросы интервьюера

Q: Чем ref отличается от state?

State предназначен для данных, изменение которых должно вызвать перерисовку. Ref — для значений, которые нужно сохранить между рендерами без перерисовки (DOM-элементы, таймеры, предыдущие значения).

Q: Когда ref.current становится доступным?

После монтирования компонента. В момент первого рендера ref.current ещё равен начальному значению (обычно null).

Q: Можно ли использовать ref в условном рендеринге?

Да, но нужно учитывать, что ref.current будет null, пока элемент не отрендерен.

Q: Зачем нужен forwardRef?

Функциональные компоненты не имеют экземпляра, поэтому ref по умолчанию не передаётся. forwardRef позволяет "пробросить" ref к DOM-элементу внутри компонента.


Источники

Code Example 1: Creating ref

❓ В чём разница между useRef и createRef? Когда какой использовать?

Functional component:

tsx
import { useRef } from 'react';
 
function MyComponent() {
  const inputRef = useRef<HTMLInputElement>(null);
 
  return <input ref={inputRef} />;
}

Class component:

tsx
import { Component, createRef } from 'react';
 
class MyComponent extends Component {
  inputRef = createRef<HTMLInputElement>();
 
  render() {
    return <input ref={this.inputRef} />;
  }
}

Code Example 2: Focus management

❓ Что делает passwordRef.current?.focus()? Когда ref.current становится доступным?

tsx
import { useRef } from 'react';
 
function LoginForm() {
  const emailRef = useRef<HTMLInputElement>(null);
  const passwordRef = useRef<HTMLInputElement>(null);
 
  const handleEmailSubmit = () => {
    passwordRef.current?.focus();
  };
 
  return (
    <form>
      <input
        ref={emailRef}
        type="email"
        placeholder="Email"
        onKeyDown={(e) => e.key === 'Enter' && handleEmailSubmit()}
      />
      <input
        ref={passwordRef}
        type="password"
        placeholder="Пароль"
      />
    </form>
  );
}

Code Example 3: Storing previous value

❓ Почему для хранения предыдущего значения используется ref, а не state? Что происходит при изменении ref.current?

tsx
import { useRef, useEffect, useState } from 'react';
 
function Counter() {
  const [count, setCount] = useState(0);
  const prevCountRef = useRef<number>(0);
 
  useEffect(() => {
    prevCountRef.current = count;
  });
 
  return (
    <div>
      <p>Текущее: {count}</p>
      <p>Предыдущее: {prevCountRef.current}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
    </div>
  );
}

Code Example 4: Third-party library integration

❓ Зачем здесь нужны два ref? Как происходит интеграция с внешней библиотекой?

tsx
import { useRef, useEffect } from 'react';
import Chart from 'chart.js';
 
function ChartComponent({ data }) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const chartRef = useRef<Chart | null>(null);
 
  useEffect(() => {
    if (canvasRef.current) {
      chartRef.current = new Chart(canvasRef.current, {
        type: 'line',
        data: data
      });
    }
 
    return () => {
      chartRef.current?.destroy();
    };
  }, [data]);
 
  return <canvas ref={canvasRef} />;
}

Code Example 5: forwardRef

❓ Почему Version A не работает? Что делает forwardRef?

Version A:

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

Version B:

tsx
const MyInput = forwardRef<HTMLInputElement, Props>((props, ref) => {
  return <input ref={ref} {...props} />;
});
 
function Parent() {
  const inputRef = useRef<HTMLInputElement>(null);
  return <MyInput ref={inputRef} />;
}

Code Example 6: Callback ref

❓ Чем callback ref отличается от объекта ref? Когда вызывается эта функция?

tsx
function MeasureExample() {
  const measureRef = (node: HTMLDivElement | null) => {
    if (node !== null) {
      console.log('Размеры:', node.getBoundingClientRect());
    }
  };
 
  return <div ref={measureRef}>Измеряемый элемент</div>;
}