Какие основные факторы вызывают повторный рендер компонента в React?

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

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

Основные факторы, вызывающие повторный рендер компонента в React:

  1. Изменение состояния (state) — вызов функции setState или setter из useState
  2. Изменение свойств (props) — когда родительский компонент передает новые пропсы
  3. Повторный рендеринг родителя — когда родительский компонент перерисовывается
  4. Изменение контекста (context) — когда данные в React Context обновляются
  5. Хуки с зависимостями — когда изменяются зависимости в хуках типа useMemo или useEffect

Пример ререндера при изменении состояния:

function Counter() {
  const [count, setCount] = useState(0);
  
  // При нажатии кнопки изменяется состояние,
  // что вызывает повторный рендер
  return (
    <div>
      <p>Счетчик: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Увеличить
      </button>
    </div>
  );
}

Полный ответ

React компоненты перерисовываются при изменении данных, от которых они зависят. Это фундаментальный принцип реактивного программирования в React, но понимание конкретных причин повторных рендеров критично для создания оптимизированных приложений. 🔍

1. Изменение состояния (State)

Любое изменение внутреннего состояния компонента вызывает его перерисовку:

function ToggleButton() {
  const [isOn, setIsOn] = useState(false);
  console.log("Компонент рендерится"); // Выполняется при каждом изменении состояния
  
  return (
    <button onClick={() => setIsOn(!isOn)}>
      {isOn ? 'Выключить' : 'Включить'}
    </button>
  );
}

При каждом нажатии кнопки isOn меняется, что запускает новый рендер.

2. Изменение пропсов (Props)

Когда компонент получает новые пропсы от родителя, он перерисовывается:

// Дочерний компонент
function Message({ text }) {
  console.log("Message рендерится");
  return <p>{text}</p>;
}
 
// Родительский компонент
function Chat() {
  const [message, setMessage] = useState("");
  
  return (
    <div>
      <input 
        value={message}
        onChange={(e) => setMessage(e.target.value)}
      />
      <Message text={message} /> {/* Перерендер при каждом изменении ввода */}
    </div>
  );
}

3. Повторный рендеринг родителя

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

function Parent() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Увеличить счетчик: {count}
      </button>
      <Child /> {/* Перерендеривается при каждом клике, хотя пропсы не меняются */}
    </div>
  );
}
 
function Child() {
  console.log("Child рендерится");
  return <div>Дочерний компонент</div>;
}

4. Изменение контекста (Context)

Компоненты, использующие React Context, перерисовываются при изменении значения контекста:

const ThemeContext = React.createContext('light');
 
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  
  return (
    <ThemeContext.Provider value={theme}>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        Переключить тему
      </button>
      {children}
    </ThemeContext.Provider>
  );
}
 
function ThemedButton() {
  const theme = useContext(ThemeContext);
  console.log("ThemedButton рендерится");
  
  return <button className={theme}>Кнопка с темой</button>;
}

При изменении темы все компоненты, использующие ThemeContext, перерисуются.

5. Обновление зависимостей в хуках

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

function SearchResults({ query }) {
  // Повторно вычисляется при изменении query
  const filteredData = useMemo(() => {
    console.log("Фильтрация выполняется");
    return expensiveFilter(query);
  }, [query]);
  
  return (
    <ul>
      {filteredData.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

Как предотвратить ненужные повторные рендеры

1. React.memo для функциональных компонентов

Предотвращает перерисовку, если пропсы не изменились:

// Не будет перерисовываться, если name не изменился
const Greeting = React.memo(function Greeting({ name }) {
  console.log("Greeting рендерится");
  return <h1>Привет, {name}!</h1>;
});

2. useMemo для вычисляемых значений

Кэширует результаты вычислений между рендерами:

function ProductList({ products, filter }) {
  // Фильтрация выполняется только при изменении products или filter
  const filteredProducts = useMemo(() => {
    return products.filter(product => product.category === filter);
  }, [products, filter]);
  
  return (
    <ul>
      {filteredProducts.map(product => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}

3. useCallback для функций

Предотвращает создание новых функций при каждом рендере:

function Parent() {
  const [count, setCount] = useState(0);
  
  // Функция не пересоздаётся при изменении count
  const handleClick = useCallback(() => {
    console.log('Клик');
  }, []);
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Счетчик: {count}
      </button>
      <ExpensiveChild onClick={handleClick} />
    </div>
  );
}
 
const ExpensiveChild = React.memo(function({ onClick }) {
  console.log("ExpensiveChild рендерится");
  return <button onClick={onClick}>Дочерняя кнопка</button>;
});

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

1. Создание новых объектов в рендере

// ❌ Плохо: новый объект стилей при каждом рендере
function Button() {
  return (
    <button 
      style={{ color: 'blue', fontSize: '14px' }} // Новый объект при каждом рендере
    >
      Нажми меня
    </button>
  );
}
 
// ✅ Хорошо: константа за пределами компонента
const buttonStyle = { color: 'blue', fontSize: '14px' };
 
function Button() {
  return <button style={buttonStyle}>Нажми меня</button>;
}

2. Инлайн функции в пропсах

// ❌ Плохо: новая функция при каждом рендере
function Parent() {
  return (
    <Child onClick={() => console.log('Клик')} /> // Новая функция при каждом рендере
  );
}
 
// ✅ Хорошо: использование useCallback
function Parent() {
  const handleClick = useCallback(() => {
    console.log('Клик');
  }, []);
  
  return <Child onClick={handleClick} />;
}

3. Неправильные зависимости хуков

// ❌ Плохо: неуказана зависимость data
function DataList({ data }) {
  useEffect(() => {
    console.log("Данные изменились:", data);
  }, []); // Зависимость data отсутствует
  
  // ... остальной код
}
 
// ✅ Хорошо: правильно указаны зависимости
function DataList({ data }) {
  useEffect(() => {
    console.log("Данные изменились:", data);
  }, [data]);
  
  // ... остальной код
}

Инструменты для отладки рендеров

  1. React DevTools Profiler — позволяет визуализировать рендеры компонентов
  2. why-did-you-render — библиотека, которая уведомляет о ненужных повторных рендерах
  3. Консольные логи — простое добавление console.log в тело компонента
function MyComponent(props) {
  console.log("MyComponent рендерится", { props });
  // ... код компонента
}

Резюме

Основные причины ререндеров в React:

  • Изменение собственного state компонента
  • Получение новых props от родителя
  • Повторный рендер родительского компонента
  • Изменение контекста (Context)
  • Изменение зависимостей в хуках

Как оптимизировать перерисовки:

  • React.memo — предотвращает рендер при неизменных пропсах
  • useMemo — кэширует результаты вычислений
  • useCallback — предотвращает пересоздание функций
  • Избегайте создания новых объектов и функций при рендере
  • Правильно указывайте зависимости в хуках

Понимание причин повторных рендеров и умение их оптимизировать — ключевой навык для разработки производительных React-приложений. Оптимизируйте осознанно, исходя из реальных проблем производительности, а не преждевременно. 🚀


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