Skip to main content

Що таке React.PureComponent

React.PureComponent - це базовий клас для класових компонентів React, який пропускає зайві ре-рендери через поверхневе порівняння (shallow comparison) props і state перед кожним рендером.

Теорія

TL;DR

  • Уяви перевірку на вході: PureComponent дивиться на етикетку коробки (посилання), а не заглядає всередину.
  • Головна відмінність від React.Component: вбудований shouldComponentUpdate з поверхневим порівнянням props і state.
  • Плоскі props (числа, рядки, стабільні посилання) - PureComponent оптимізує без зусиль. Вкладені мутації - пропустить зміну.
  • Для функціональних компонентів використовуй React.memo.

Швидкий приклад

tsx
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.ComponentReact.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 і переходить до наступного завдання.

Типові помилки

Мутація об'єктів або масивів на місці:

tsx
// Неправильно: те саме посилання на масив, 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.

Передача нового об'єктного літерала при кожному рендері:

tsx
// Неправильно: новий об'єкт при кожному рендері знищує будь-яку користь <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.

tsx
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 у продакшені:

tsx
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 показує старий вік. Виправлення займає один рядок: розгорни об'єкт, щоб створити нове посилання.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?