Hack Frontend Community

Reconciliation (Согласование) в React

Что такое Reconciliation?

Reconciliation (Согласование) — это алгоритм, который React использует для сравнения двух деревьев элементов и определения, какие части реального DOM нужно обновить.

Когда state или props компонента меняются, React создаёт новое дерево элементов и сравнивает его с предыдущим, чтобы сделать минимальное количество изменений в DOM.


Зачем это нужно?

Обновление DOM — дорогая операция. React оптимизирует этот процесс:

  1. Создаёт виртуальную копию DOM (Virtual DOM)
  2. При изменениях создаёт новое дерево
  3. Сравнивает старое и новое дерево (diffing)
  4. Обновляет только изменённые части реального DOM

Как работает алгоритм

Основные правила

React использует эвристический алгоритм O(n) вместо O(n³). Он основан на двух предположениях:

  1. Элементы разных типов создают разные деревья
  2. Разработчик может указать, какие элементы стабильны с помощью key

Сравнение элементов разных типов

Когда корневые элементы имеют разные типы, React удаляет старое дерево и строит новое с нуля.

// Старое дерево
<div>
  <Counter />
</div>

// Новое дерево
<span>
  <Counter />
</span>

React:

  1. Удаляет <div> и всех детей
  2. Размонтирует Counter (вызовет componentWillUnmount)
  3. Создаёт новый <span>
  4. Монтирует новый Counter (вызовет componentDidMount)

Сравнение элементов одного типа

DOM-элементы

Когда типы совпадают, React сравнивает атрибуты и обновляет только изменённые:

// Старый
<div className="before" title="old" />

// Новый
<div className="after" title="old" />

React обновит только className, title останется без изменений.

Стили

// Старый
<div style={{color: 'red', fontWeight: 'bold'}} />

// Новый
<div style={{color: 'blue', fontWeight: 'bold'}} />

React обновит только color.


Сравнение компонентов одного типа

Когда компонент обновляется, экземпляр остаётся тем же, поэтому state сохраняется:

<Counter count={1} />

// После обновления
<Counter count={2} />

React:

  1. Обновляет props компонента
  2. Вызывает componentWillReceiveProps() и componentWillUpdate()
  3. Вызывает render()
  4. Запускает reconciliation для результата render()

Рекурсивная обработка детей

React рекурсивно обрабатывает детей. Рассмотрим пример:

Добавление элемента в конец

// Старый
<ul>
  <li>first</li>
  <li>second</li>
</ul>

// Новый
<ul>
  <li>first</li>
  <li>second</li>
  <li>third</li>
</ul>

React:

  1. Сравнивает первый <li> — одинаковые, ничего не делает
  2. Сравнивает второй <li> — одинаковые, ничего не делает
  3. Добавляет третий <li>

Эффективно!

Добавление элемента в начало (без key)

// Старый
<ul>
  <li>first</li>
  <li>second</li>
</ul>

// Новый
<ul>
  <li>zero</li>
  <li>first</li>
  <li>second</li>
</ul>

React:

  1. Сравнивает первый <li> — "first" ≠ "zero", обновляет
  2. Сравнивает второй <li> — "second" ≠ "first", обновляет
  3. Добавляет третий <li>

Неэффективно! React не понял, что элементы просто сдвинулись.


Ключи (Keys)

key помогает React понять, какие элементы изменились, добавились или удалились.

С ключами

// Старый
<ul>
  <li key="first">first</li>
  <li key="second">second</li>
</ul>

// Новый
<ul>
  <li key="zero">zero</li>
  <li key="first">first</li>
  <li key="second">second</li>
</ul>

React:

  1. Видит новый key "zero" — создаёт элемент
  2. Видит key "first" — элемент уже был, оставляет
  3. Видит key "second" — элемент уже был, оставляет

Эффективно!


Правила использования keys

Уникальность среди соседей

Ключи должны быть уникальными только среди соседних элементов:

function Blog(props) {
  const sidebar = (
    <ul>
      {props.posts.map(post =>
        <li key={post.id}>{post.title}</li>
      )}
    </ul>
  );
  
  const content = props.posts.map(post =>
    <div key={post.id}>
      <h3>{post.title}</h3>
      <p>{post.content}</p>
    </div>
  );
  
  return (
    <div>
      {sidebar}
      <hr />
      {content}
    </div>
  );
}

Одинаковые key в разных массивах — это нормально.

Стабильность ключей

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

// Плохо
{items.map(item => <Item key={Math.random()} data={item} />)}

// Плохо (при пересортировке потеряется state)
{items.map((item, index) => <Item key={index} data={item} />)}

// Хорошо
{items.map(item => <Item key={item.id} data={item} />)}

Почему index — плохая идея?

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Buy milk', done: false },
    { id: 2, text: 'Clean room', done: true }
  ]);

  // Плохо
  return (
    <ul>
      {todos.map((todo, index) => (
        <TodoItem key={index} todo={todo} />
      ))}
    </ul>
  );
}

Проблема: при удалении первого элемента, индексы смещаются, и React думает, что изменился контент, а не порядок.

// Хорошо
return (
  <ul>
    {todos.map(todo => (
      <TodoItem key={todo.id} todo={todo} />
    ))}
  </ul>
);

Оптимизации

shouldComponentUpdate

Можно оптимизировать reconciliation, сказав React, когда компонент НЕ нужно перерисовывать:

class MyComponent extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    // Рендерить только если изменился id
    return this.props.id !== nextProps.id;
  }

  render() {
    return <div>{this.props.data}</div>;
  }
}

React.PureComponent

Автоматически делает поверхностное сравнение props и state:

class MyComponent extends React.PureComponent {
  render() {
    return <div>{this.props.data}</div>;
  }
}

React.memo

Для функциональных компонентов:

const MyComponent = React.memo(function MyComponent(props) {
  return <div>{props.data}</div>;
});

// С кастомным сравнением
const MyComponent = React.memo(
  function MyComponent(props) {
    return <div>{props.data}</div>;
  },
  (prevProps, nextProps) => {
    return prevProps.id === nextProps.id;
  }
);

Примеры из практики

Список с фильтрацией

function UserList({ users, filter }) {
  const filteredUsers = users.filter(user =>
    user.name.includes(filter)
  );

  return (
    <ul>
      {filteredUsers.map(user => (
        <li key={user.id}>
          {user.name}
          <input type="text" />
        </li>
      ))}
    </ul>
  );
}

Если использовать index вместо user.id, то при изменении фильтра значения в <input> перемешаются, потому что React не поймёт, что это другие пользователи.

Переключение между компонентами

function App() {
  const [tab, setTab] = useState('profile');

  return (
    <div>
      {tab === 'profile' && <Profile />}
      {tab === 'settings' && <Settings />}
    </div>
  );
}

При переключении tab React полностью размонтирует один компонент и монтирует другой, потому что это разные типы.

Если хотите сохранить state, используйте display: none:

function App() {
  const [tab, setTab] = useState('profile');

  return (
    <div>
      <div style={{ display: tab === 'profile' ? 'block' : 'none' }}>
        <Profile />
      </div>
      <div style={{ display: tab === 'settings' ? 'block' : 'none' }}>
        <Settings />
      </div>
    </div>
  );
}

Частые ошибки

Изменение типа элемента

function Component({ isLoading }) {
  if (isLoading) {
    return <div>Loading...</div>;
  }
  return <span>Data</span>;
}

При изменении isLoading React размонтирует весь компонент, потому что divspan.

Решение: используйте одинаковый тип:

function Component({ isLoading }) {
  return <div>{isLoading ? 'Loading...' : 'Data'}</div>;
}

Отсутствие keys в списках

// Плохо
function List({ items }) {
  return (
    <ul>
      {items.map(item => <li>{item.name}</li>)}
    </ul>
  );
}

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

Нестабильные keys

// Плохо
function List({ items }) {
  return (
    <ul>
      {items.map(item => (
        <li key={`${item.name}-${Date.now()}`}>
          {item.name}
        </li>
      ))}
    </ul>
  );
}

Key меняется при каждом рендере, React будет пересоздавать элементы.


Вывод

Reconciliation:

  • Алгоритм сравнения деревьев элементов
  • Работает за O(n) благодаря эвристикам
  • Разные типы элементов = полная пересборка
  • Одинаковые типы = обновление атрибутов
  • Keys помогают идентифицировать элементы в списках
  • Можно оптимизировать с помощью shouldComponentUpdate, PureComponent, memo

На собеседовании:

Важно уметь:

  • Объяснить, что такое reconciliation и зачем он нужен
  • Рассказать, как React сравнивает элементы разных и одинаковых типов
  • Объяснить важность keys в списках
  • Привести примеры, когда index как key — плохая идея
  • Рассказать о способах оптимизации (PureComponent, memo)