Что такое «чистые» компоненты (Pure Components)?

👨‍💻 Frontend Developer 🟠 Может встретиться 🎚️ Средний
#React

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

Чистые компоненты (Pure Components) — это компоненты React, которые автоматически реализуют поверхностное сравнение props и state для оптимизации производительности. Они перерендериваются только при изменении входных данных.

Основные характеристики:

  • Автоматическая оптимизация через поверхностное сравнение
  • Предотвращают ненужные перерендеры
  • Работают только с иммутабельными данными
  • Доступны как для классовых, так и для функциональных компонентов

Ограничения:

  • Не работают с мутирующими данными
  • Могут давать неожиданные результаты при неправильном использовании
  • Поверхностное сравнение не подходит для сложных объектов

Ключевое правило: Используйте чистые компоненты только с иммутабельными данными и при правильной работе с состоянием.


Полный ответ

Чистые компоненты — это специальный тип компонентов React, которые автоматически оптимизируют производительность за счет реализации поверхностного сравнения props и state. Они перерендериваются только в том случае, если изменились входные данные.

Что такое чистые компоненты

Чистые компоненты реализуют метод shouldComponentUpdate с поверхностным сравнением (shallow comparison) props и state:

// Обычный компонент - перерендеривается всегда
class RegularComponent extends Component {
  render() {
    console.log('RegularComponent рендерится');
    return <div>{this.props.value}</div>;
  }
}
 
// Чистый компонент - перерендеривается только при изменении props/state
class PureComponentExample extends PureComponent {
  render() {
    console.log('PureComponent рендерится');
    return <div>{this.props.value}</div>;
  }
}
 
// Использование
function App() {
  const [count, setCount] = useState(0);
  
  // Эти данные не изменяются
  const data = { name: 'Александр' };
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Увеличить: {count}
      </button>
      
      {/* Обычный компонент будет перерендериваться при каждом рендере App */}
      <RegularComponent value={data.name} />
      
      {/* Чистый компонент перерендерится только если data.name изменится */}
      <PureComponentExample value={data.name} />
    </div>
  );
}

Как работают чистые компоненты

1. Поверхностное сравнение

Чистые компоненты используют поверхностное сравнение props и state:

// Пример поверхностного сравнения
function shallowEqual(objA, objB) {
  if (objA === objB) {
    return true;
  }
  
  if (typeof objA !== 'object' || objA === null ||
      typeof objB !== 'object' || objB === null) {
    return false;
  }
  
  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);
  
  if (keysA.length !== keysB.length) {
    return false;
  }
  
  // Сравниваем только свойства первого уровня
  for (let i = 0; i < keysA.length; i++) {
    if (!objB.hasOwnProperty(keysA[i]) || 
        objA[keysA[i]] !== objB[keysA[i]]) {
      return false;
    }
  }
  
  return true;
}

2. Разница между обычными и чистыми компонентами

// Обычный компонент
class RegularCounter extends Component {
  render() {
    return (
      <div>
        <p>Счетчик: {this.props.count}</p>
        <button onClick={this.props.onIncrement}>
          Увеличить
        </button>
      </div>
    );
  }
}
 
// Чистый компонент
class PureCounter extends PureComponent {
  render() {
    return (
      <div>
        <p>Счетчик: {this.props.count}</p>
        <button onClick={this.props.onIncrement}>
          Увеличить
        </button>
      </div>
    );
  }
}
 
// Демонстрация разницы
function App() {
  const [count, setCount] = useState(0);
  const [otherState, setOtherState] = useState('');
  
  // Объект, который не изменяется
  const config = { theme: 'light' };
  
  const handleIncrement = () => {
    setCount(count + 1);
  };
  
  return (
    <div>
      <input 
        value={otherState} 
        onChange={(e) => setOtherState(e.target.value)} 
        placeholder="Другое состояние"
      />
      
      {/* RegularCounter перерендерится при изменении otherState */}
      <RegularCounter count={count} onIncrement={handleIncrement} />
      
      {/* PureCounter перерендерится только при изменении count */}
      <PureCounter count={count} onIncrement={handleIncrement} />
    </div>
  );
}

Чистые компоненты в функциональных компонентах

С появлением хуков, функциональные компоненты могут использовать мемоизацию:

import { memo, useState } from 'react';
 
// Обычный функциональный компонент
function RegularComponent({ name, age }) {
  console.log('RegularComponent рендерится');
  return (
    <div>
      <h1>{name}</h1>
      <p>Возраст: {age}</p>
    </div>
  );
}
 
// Мемоизированный компонент (аналог PureComponent)
const MemoizedComponent = memo(function MemoizedComponent({ name, age }) {
  console.log('MemoizedComponent рендерится');
  return (
    <div>
      <h1>{name}</h1>
      <p>Возраст: {age}</p>
    </div>
  );
});
 
// С пользовательской функцией сравнения
const CustomMemoizedComponent = memo(
  function CustomMemoizedComponent({ user }) {
    console.log('CustomMemoizedComponent рендерится');
    return (
      <div>
        <h1>{user.name}</h1>
        <p>Возраст: {user.age}</p>
      </div>
    );
  },
  // Пользовательская функция сравнения
  (prevProps, nextProps) => {
    return prevProps.user.name === nextProps.user.name && 
           prevProps.user.age === nextProps.user.age;
  }
);
 
// Использование
function App() {
  const [count, setCount] = useState(0);
  
  // Эти данные не изменяются между рендерами
  const userData = { name: 'Александр', age: 25 };
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Счетчик: {count}
      </button>
      
      <RegularComponent name={userData.name} age={userData.age} />
      <MemoizedComponent name={userData.name} age={userData.age} />
      <CustomMemoizedComponent user={userData} />
    </div>
  );
}

Когда использовать чистые компоненты

✅ Подходят для:

// 1. Компоненты с примитивными props
function UserInfo({ name, age, email }) {
  return (
    <div>
      <h2>{name}</h2>
      <p>Возраст: {age}</p>
      <p>Email: {email}</p>
    </div>
  );
}
const PureComponentUserInfo = memo(UserInfo);
 
// 2. Компоненты с иммутабельными объектами
function TodoItem({ todo, onToggle, onDelete }) {
  return (
    <div className={todo.completed ? 'completed' : ''}>
      <span>{todo.text}</span>
      <button onClick={() => onToggle(todo.id)}>Переключить</button>
      <button onClick={() => onDelete(todo.id)}>Удалить</button>
    </div>
  );
}
const PureComponentTodoItem = memo(TodoItem);
 
// 3. Компоненты с массивами (если массив пересоздается при изменении)
function TodoList({ todos, onToggle, onDelete }) {
  return (
    <div>
      {todos.map(todo => (
        <PureComponentTodoItem 
          key={todo.id} 
          todo={todo} 
          onToggle={onToggle}
          onDelete={onDelete}
        />
      ))}
    </div>
  );
}

❌ Не подходят для:

// 1. Компоненты с мутирующими объектами
class BadExample extends PureComponent {
  render() {
    // ❌ Если parentData мутирует, компонент не перерендерится
    return <div>{this.props.parentData.value}</div>;
  }
}
 
// 2. Компоненты с функциями, создаваемыми в родителе
function Parent() {
  const [count, setCount] = useState(0);
  
  // ❌ Новая функция при каждом рендере
  const handleClick = () => {
    console.log('Кликнули!');
  };
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Счетчик: {count}
      </button>
      {/* Компонент будет перерендериваться из-за новой функции */}
      <PureComponentWithFunction onClick={handleClick} />
    </div>
  );
}
 
// ✅ Правильный подход - мемоизация функции
function GoodParent() {
  const [count, setCount] = useState(0);
  
  // ✅ Функция создается один раз
  const handleClick = useCallback(() => {
    console.log('Кликнули!');
  }, []);
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Счетчик: {count}
      </button>
      {/* Компонент не будет перерендериваться */}
      <PureComponentWithFunction onClick={handleClick} />
    </div>
  );
}

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

1. Оптимизация списка элементов

import { memo, useState, useCallback } from 'react';
 
// Чистый компонент для элемента списка
const ListItem = memo(function ListItem({ item, onUpdate, onDelete }) {
  console.log(`ListItem ${item.id} рендерится`);
  
  return (
    <div className="list-item">
      <span>{item.text}</span>
      <button onClick={() => onUpdate(item.id, `${item.text} (обновлено)`)}>
        Обновить
      </button>
      <button onClick={() => onDelete(item.id)}>
        Удалить
      </button>
    </div>
  );
});
 
// Компонент списка
const List = memo(function List({ items, onUpdate, onDelete }) {
  console.log('List рендерится');
  
  return (
    <div>
      {items.map(item => (
        <ListItem 
          key={item.id} 
          item={item} 
          onUpdate={onUpdate}
          onDelete={onDelete}
        />
      ))}
    </div>
  );
});
 
// Родительский компонент
function App() {
  const [items, setItems] = useState([
    { id: 1, text: 'Элемент 1' },
    { id: 2, text: 'Элемент 2' },
    { id: 3, text: 'Элемент 3' }
  ]);
  
  const [otherState, setOtherState] = useState('');
  
  // Мемоизированные функции
  const handleUpdate = useCallback((id, newText) => {
    setItems(prevItems => 
      prevItems.map(item => 
        item.id === id ? { ...item, text: newText } : item
      )
    );
  }, []);
  
  const handleDelete = useCallback((id) => {
    setItems(prevItems => 
      prevItems.filter(item => item.id !== id)
    );
  }, []);
  
  return (
    <div>
      <input 
        value={otherState}
        onChange={(e) => setOtherState(e.target.value)}
        placeholder="Другое состояние"
      />
      
      {/* List перерендерится только при изменении items */}
      <List 
        items={items} 
        onUpdate={handleUpdate}
        onDelete={handleDelete}
      />
    </div>
  );
}

2. Работа с вложенными объектами

import { memo, useState } from 'react';
 
// ❌ Проблема с вложенными объектами
const UserProfileBad = memo(function UserProfileBad({ user }) {
  return (
    <div>
      <h2>{user.profile.name}</h2>
      <p>Возраст: {user.profile.age}</p>
      <p>Email: {user.contact.email}</p>
    </div>
  );
});
 
// ✅ Правильный подход - деструктуризация или пользовательское сравнение
const UserProfileGood = memo(
  function UserProfileGood({ user }) {
    return (
      <div>
        <h2>{user.profile.name}</h2>
        <p>Возраст: {user.profile.age}</p>
        <p>Email: {user.contact.email}</p>
      </div>
    );
  },
  // Пользовательская функция сравнения для вложенных объектов
  (prevProps, nextProps) => {
    return (
      prevProps.user.profile.name === nextProps.user.profile.name &&
      prevProps.user.profile.age === nextProps.user.profile.age &&
      prevProps.user.contact.email === nextProps.user.contact.email
    );
  }
);
 
// Альтернативный подход - передача отдельных props
const UserProfileBest = memo(function UserProfileBest({ name, age, email }) {
  return (
    <div>
      <h2>{name}</h2>
      <p>Возраст: {age}</p>
      <p>Email: {email}</p>
    </div>
  );
});

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

1. Использование с мутирующими данными

// ❌ Неправильно
class BadParent extends Component {
  constructor(props) {
    super(props);
    this.state = {
      data: { value: 0 }
    };
  }
  
  handleClick = () => {
    // Мутируем существующий объект
    this.state.data.value += 1;
    this.setState({ data: this.state.data }); // ❌ Не создаем новый объект!
  };
  
  render() {
    return (
      <div>
        <button onClick={this.handleClick}>
          Увеличить
        </button>
        {/* PureComponent не перерендерится, так как объект тот же */}
        <PureChild data={this.state.data} />
      </div>
    );
  }
}
 
// ✅ Правильно
class GoodParent extends Component {
  constructor(props) {
    super(props);
    this.state = {
      data: { value: 0 }
    };
  }
  
  handleClick = () => {
    // Создаем новый объект
    this.setState({
      data: { value: this.state.data.value + 1 } // ✅ Новый объект
    });
  };
  
  render() {
    return (
      <div>
        <button onClick={this.handleClick}>
          Увеличить
        </button>
        {/* PureComponent перерендерится, так как объект новый */}
        <PureChild data={this.state.data} />
      </div>
    );
  }
}

2. Создание функций в рендере

// ❌ Неправильно
function BadParent() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Счетчик: {count}
      </button>
      {/* Новая функция при каждом рендере - PureComponent перерендерится */}
      <PureComponent onClick={() => console.log('Клик!')} />
    </div>
  );
}
 
// ✅ Правильно
function GoodParent() {
  const [count, setCount] = useState(0);
  
  // Функция создается один раз
  const handleClick = useCallback(() => {
    console.log('Клик!');
  }, []);
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Счетчик: {count}
      </button>
      {/* Та же функция при каждом рендере - PureComponent не перерендерится */}
      <PureComponent onClick={handleClick} />
    </div>
  );
}

Резюме

Чистые компоненты — это компоненты React, которые автоматически оптимизируют производительность через поверхностное сравнение props и state:

Преимущества:

  • Автоматическая оптимизация перерендеров
  • Повышение производительности при правильном использовании
  • Простота реализации

Ограничения:

  • Работают только с иммутабельными данными
  • Поверхностное сравнение не подходит для сложных объектов
  • Могут давать неожиданные результаты при неправильном использовании

Ключевые моменты:

  • Используйте чистые компоненты с примитивными props и иммутабельными объектами
  • Для функциональных компонентов используйте memo
  • Избегайте мутирующих данных и функций, создаваемых в рендере
  • При работе с вложенными объектами используйте пользовательские функции сравнения

Понимание чистых компонентов помогает создавать более производительные React-приложения и избегать ненужных перерендеров.


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