Що таке React.PureComponent
React.PureComponent - це базовий клас для класових компонентів React, який пропускає зайві ре-рендери через поверхневе порівняння (shallow comparison) props і state перед кожним рендером.
Теорія
TL;DR
- Уяви перевірку на вході:
PureComponentдивиться на етикетку коробки (посилання), а не заглядає всередину. - Головна відмінність від
React.Component: вбудованийshouldComponentUpdateз поверхневим порівнянням props і state. - Плоскі props (числа, рядки, стабільні посилання) -
PureComponentоптимізує без зусиль. Вкладені мутації - пропустить зміну. - Для функціональних компонентів використовуй
React.memo.
Швидкий приклад
import React, { PureComponent, Component } from 'react';
class PureChild extends PureComponent<{ count: number }> {
render() {
console.log('PureChild renders');
return <div>Count: {this.props.count}</div>;
}
}
class RegularChild extends Component<{ count: number }> {
render() {
console.log('RegularChild renders');
return <div>Count: {this.props.count}</div>;
}
}
class Parent extends Component {
state = { count: 0 };
render() {
return (
<>
<PureChild count={this.state.count} />
<RegularChild count={this.state.count} />
{/* count залишається 0, але батько все одно викликає setState */}
<button onClick={() => this.setState({ count: this.state.count })}>
Без змін
</button>
</>
);
}
}
// Клік: тільки "RegularChild renders" в консолі. PureChild пропускає.Обидва компоненти отримують однакове значення count після кліку. RegularChild рендериться все одно. PureChild перевіряє, нічого не знаходить і виходить без рендеру.
Ключова відмінність
React.Component ре-рендериться кожного разу, коли батько оновлюється або викликається setState, незалежно від того, чи реально змінилися дані. React.PureComponent перевизначає shouldComponentUpdate з поверхневим порівнянням: перебирає кожен ключ props і state та порівнює значення через Object.is. Якщо нічого не змінилось, React пропускає рендер і diff усього піддерева. Поверхнева перевірка коштує невеликих ресурсів CPU, але це майже завжди дешевше за diff цілого піддерева.
Коли використовувати
- Props - примітиви (числа, рядки, булеві значення):
PureComponentдає безкоштовну оптимізацію без жодного додаткового коду. - Об'єкти в props стабільні (мемоізовані через
useMemoабо створені один раз поза рендером): поверхнева перевірка спрацює правильно. - Список з 50-100+ елементів, де батько часто ре-рендериться, а самі елементи змінюються рідко:
PureComponentсуттєво скорочує зайві рендери. - Вкладені об'єкти мутують на місці: не використовуй
PureComponent, він пропустить зміну і покаже застарілий UI. - Функціональний компонент: використовуй
React.memo- та сама логіка, але підтримує хуки.
Таблиця порівняння
| Властивість | React.Component | React.PureComponent |
|---|---|---|
| Тригер ре-рендеру | Будь-яке оновлення батька або setState | Оновлення батька + реальна зміна props/state |
| shouldComponentUpdate | Немає за замовчуванням | Вбудована поверхнева перевірка |
| Мутації об'єктів на місці | Помітить (завжди рендериться) | Не помітить (пропустить) |
| Підходить для | Будь-якої форми props | Плоских або стабільних props |
| Еквівалент для функцій | Немає | React.memo |
Як це працює всередині
PureComponent встановлює прапор isPureReactComponent = true, який читає reconciler React. Перед викликом render React запускає вбудований shouldComponentUpdate, що використовує Object.is для порівняння кожного ключа в props і state. Якщо всі порівняння збігаються, компонент і все піддерево пропускають reconciliation. У React 16+ з fiber цей bail-out ефективний: React не викликає render і переходить до наступного завдання.
Типові помилки
Мутація об'єктів або масивів на місці:
// Неправильно: те саме посилання на масив, PureComponent пропускає ре-рендер
this.props.items[0].done = true;
this.forceUpdate(); // хак, і все одно не допоможе дочірнім PureComponent
// Правильно: нове посилання
this.setState({
items: this.state.items.map((item, i) =>
i === 0 ? { ...item, done: true } : item
)
});PureComponent бачить те саме посилання на масив і пропускає рендер. UI залишається застарілим. Це найпоширеніший баг у продакшені з PureComponent.
Передача нового об'єктного літерала при кожному рендері:
// Неправильно: новий об'єкт при кожному рендері знищує будь-яку користь
<PureChild config={{ theme: 'dark' }} />
// Правильно: стабільне посилання
const config = useMemo(() => ({ theme: 'dark' }), []);
<PureChild config={config} />Отримуєш витрати на поверхневу перевірку без жодної користі. PureComponent ре-рендериться при кожному циклі.
Очікувати перевірки вкладених даних:
Якщо ти розгортаєш об'єкт верхнього рівня, але мутуєш вкладену властивість на місці, поведінка стає непередбачуваною залежно від того, чи змінилось посилання верхнього рівня. Завжди копіюй і вкладені об'єкти: { ...user, details: { ...user.details, age: 31 } }. Все інше - баг, який чекає свого часу.
Де зустрічається в реальних проектах
- Великі таблиці та списки: кожен рядок у
PureComponent, щоб ре-рендерився тільки змінений елемент. - Material-UI TableRow і таблиця Ant Design використовують
PureComponentвсередині саме з цієї причини. - Redux-підключені списки:
PureComponentразом з reselect-селекторами, щоб компоненти ре-рендерились тільки при зміні їхнього шматка state. - React DevTools Profiler: додай
PureComponentдо підозрілого компонента і перевір "why did this render?", щоб побачити пропуски рендерів.
Особисто я знаходжу PureComponent найкориснішим при рендерингу списків. Додай 100 елементів, переключи один - і в Profiler видно, як 99 компонентів пропускають рендер без жодних додаткових зусиль.
Питання на співбесіді
Q: В чому різниця між shallow і deep рівністю?
A: Shallow перевіряє посилання кожного ключа через Object.is, лише на один рівень вглиб. Deep рекурсивно порівнює вкладені значення. Deep - дорого і рідко потрібно, якщо дотримуватись іммутабельності.
Q: Чи можна перевизначити shouldComponentUpdate на PureComponent?
A: Так. Твій варіант повністю замінює вбудовану поверхневу перевірку. Роби це тільки якщо потрібна кастомна логіка порівняння для конкретних props.
Q: Який еквівалент PureComponent для функціональних компонентів?
A: React.memo. Обгортає функціональний компонент і виконує те саме поверхневе порівняння props. Другим аргументом можна передати кастомну функцію порівняння.
Q: Чи впливає PureComponent на error boundaries?
A: Ні. Якщо render кинув помилку, вона все одно спливе до найближчого error boundary незалежно від поверхневої перевірки.
Q: У React 18 concurrent mode чи допомагає PureComponent з асинхронним рендерингом?
A: Перевірка PureComponent синхронна. Для переваг асинхронного рендерингу комбінуй React.memo з useDeferredValue. PureComponent сам по собі не взаємодіє з concurrent-функціями.
Приклади
Оптимізація рядка списку
Реальний сценарій: 100+ елементів todo. Батько ре-рендериться при будь-якій зміні, але кожен рядок має ре-рендеритися тільки якщо змінився власний todo.
interface Todo {
id: number;
text: string;
done: boolean;
}
class TodoItem extends React.PureComponent<{ todo: Todo }> {
render() {
const { todo } = this.props;
console.log(`Рендер todo #${todo.id}`);
return (
<li style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
{todo.text}
</li>
);
}
}
// Батько ре-рендерить список при кожній зміні.
// Переключи todo #3 -> тільки "Рендер todo #3" в консолі. Решта 99 пропускають.Кожен TodoItem отримує об'єкт todo з іммутабельного оновлення (новий об'єкт тільки для зміненого елемента, ті самі посилання для решти). PureComponent бачить незмінені посилання і пропускає ці рендери.
Пастка з мутацією
Цей edge case викликає застарілий UI у продакшені:
class UserCard extends React.PureComponent<{ user: { name: string; age: number } }> {
render() {
console.log('UserCard renders');
return <div>{this.props.user.name}, вік {this.props.user.age}</div>;
}
}
class App extends React.Component {
state = { user: { name: 'Alice', age: 30 } };
updateAge = () => {
// Неправильно: мутуємо той самий об'єкт, потім setState з тим самим посиланням
this.state.user.age = 31;
this.setState({ user: this.state.user }); // те саме посилання!
// PureComponent пропускає - UI показує вік 30, реальне значення 31
};
render() {
return (
<>
<UserCard user={this.state.user} />
<button onClick={this.updateAge}>День народження</button>
</>
);
}
}
// Виправлення: this.setState({ user: { ...this.state.user, age: 31 } })Поверхнева перевірка бачить те саме посилання user і пропускає рендер. UI показує старий вік. Виправлення займає один рядок: розгорни об'єкт, щоб створити нове посилання.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.