Пошук уроків, статей та іншого контенту
Пояснимо, коли компонент ререндериться та як reconciliation визначає мінімальний набір змін у DOM.
Ререндер — це повторний виклик функції компонента, під час якого React отримує нове дерево елементів React.
Ререндер не означає, що React повністю перебудовує DOM. Спочатку React порівнює нове дерево з попереднім, а потім застосовує до DOM лише необхідні зміни.
function Greeting({ name }) {
console.log("Greeting викликано");
return <h1>Привіт, {name}!</h1>;
}Коли Greeting ререндериться, його функція виконується знову. React знову отримує опис:
<h1>Привіт, Олено!</h1>Але сам DOM-елемент <h1> не обов’язково створюється заново.
Компонент може ререндеритися в кількох основних випадках.
Виклик функції-оновлювача, яку повертає useState, повідомляє React, що компонент потрібно оновити.
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
console.log("Counter ререндериться");
return (
<div>
<p>Лічильник: {count}</p>
<button onClick={() => setCount(count + 1)}>
Збільшити
</button>
</div>
);
}Після натискання кнопки:
викликається setCount;
React планує оновлення компонента;
функція Counter виконується знову;
створюється новий опис JSX;
React порівнює його з попереднім;
текст у <p> оновлюється в DOM.
Якщо батьківський компонент передає дочірньому нове значення через props, дочірній компонент ререндериться.
import { useState } from "react";
function Child({ value }) {
console.log("Child ререндериться");
return <p>Значення: {value}</p>;
}
export default function Parent() {
const [value, setValue] = useState(0);
return (
<div>
<Child value={value} />
<button onClick={() => setValue(value + 1)}>
Змінити значення
</button>
</div>
);
}Коли value змінюється, Parent передає Child нове значення, тому Child також ререндериться.
За замовчуванням ререндер батьківського компонента призводить до повторного виклику його дочірніх компонентів.
import { useState } from "react";
function Child() {
console.log("Child ререндериться");
return <p>Дочірній компонент</p>;
}
export default function Parent() {
const [count, setCount] = useState(0);
console.log("Parent ререндериться");
return (
<div>
<p>Лічильник: {count}</p>
<Child />
<button onClick={() => setCount(count + 1)}>
Збільшити
</button>
</div>
);
}У цьому прикладі Child не отримує props і його результат не змінюється. Проте за замовчуванням React все одно викликає Child під час ререндеру Parent.
Це ще не означає, що React змінить DOM усередині Child. Після порівняння React може визначити, що в DOM нічого змінювати не потрібно.
Якщо компонент використовує значення через React Context, він ререндериться, коли значення цього контексту змінюється.
Це працює подібно до props, але значення передається не безпосередньо через кожен рівень компонентів.
Важливо розрізняти два етапи:
Render phase — React викликає компоненти та створює нове дерево React-елементів.
Commit phase — React застосовує необхідні зміни до реального DOM.
Наприклад:
function Title() {
console.log("Компонент викликано");
return <h1>Заголовок</h1>;
}Якщо Title ререндериться, повідомлення в консолі з’явиться знову. Проте якщо результат залишився таким самим, React не має потреби замінювати <h1> у DOM.
Отже:
ререндер — це повторне обчислення JSX;
оновлення DOM — це застосування фактичних змін;
ререндер може відбутися навіть без змін у DOM.
Reconciliation — це процес, під час якого React порівнює попереднє дерево елементів із новим і визначає, які зміни потрібно застосувати до DOM.
React не порівнює весь HTML-рядок. Він працює з деревом React-елементів.
Розглянемо зміну тексту:
// Попередній результат
<h1>Лічильник: 0</h1>
// Новий результат
<h1>Лічильник: 1</h1>Тип елемента залишився тим самим — h1. Тому React збереже існуючий DOM-елемент і змінить лише його текст.
Приклад із класом:
// Попередній результат
<button className="primary">Зберегти</button>
// Новий результат
<button className="secondary">Зберегти</button>React збереже сам <button>, але оновить його className.
Якщо тип елемента змінюється, React може видалити старий елемент і створити новий:
// Попередній результат
<p>Повідомлення</p>
// Новий результат
<h2>Повідомлення</h2>У цьому випадку p і h2 — різні типи елементів.
React порівнює елементи приблизно зліва направо, намагаючись зберегти вже існуючі DOM-вузли.
Наприклад:
// Попередній список
<ul>
<li>Яблуко</li>
<li>Груша</li>
</ul>
// Новий список
<ul>
<li>Яблуко</li>
<li>Груша</li>
<li>Слива</li>
</ul>React може залишити перші два <li> без змін і додати лише третій.
Але для списків важливо правильно вказувати key.
key — це стабільний ідентифікатор елемента списку. Він допомагає React зрозуміти, який елемент:
залишився тим самим;
був доданий;
був видалений;
змінив позицію.
function TodoList({ todos }) {
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}Якщо список змінюється, React використовує todo.id, щоб правильно зіставити старі та нові елементи.
Нехай спочатку список такий:
[
{ id: 1, text: "Вивчити JSX" },
{ id: 2, text: "Вивчити state" }
]Після додавання елемента:
[
{ id: 3, text: "Вивчити props" },
{ id: 1, text: "Вивчити JSX" },
{ id: 2, text: "Вивчити state" }
]Завдяки key React розуміє, що перші два старі елементи не були змінені — вони просто перемістилися. React може зберегти їхній стан і DOM-вузли.
Іноді елементи списку мають такий код:
{items.map((item, index) => (
<li key={index}>{item.name}</li>
))}Такий варіант може працювати для списку, який ніколи не змінюється. Але він небезпечний, якщо елементи можуть додаватися, видалятися або переміщуватися.
Розглянемо список:
[
"Перше",
"Друге"
]Ключі будуть:
0 — Перше
1 — ДругеЯкщо додати елемент на початок:
[
"Нове",
"Перше",
"Друге"
]Ключі стануть:
0 — Нове
1 — Перше
2 — ДругеReact може вирішити, що елемент із ключем 0 — це той самий елемент, хоча насправді на його місці вже інші дані.
Тому краще використовувати стабільний унікальний ідентифікатор:
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}key має бути:
унікальним серед сусідніх елементів;
стабільним між рендерами;
пов’язаним із самим елементом, а не з його поточною позицією.
У цьому прикладі зміна лічильника спричиняє ререндер App і Message. React оновлює лише текст лічильника, а список узгоджує за key.
import { useState } from "react";
function Message({ count }) {
console.log("Message ререндериться");
return <p>Натискань: {count}</p>;
}
function TaskList({ tasks }) {
return (
<ul>
{tasks.map((task) => (
<li key={task.id}>{task.title}</li>
))}
</ul>
);
}
export default function App() {
const [count, setCount] = useState(0);
const [tasks, setTasks] = useState([
{ id: 1, title: "Вивчити ререндер" },
{ id: 2, title: "Вивчити reconciliation" },
]);
console.log("App ререндериться");
function addTask() {
setTasks((currentTasks) => [
...currentTasks,
{
id: Date.now(),
title: "Повторити приклади",
},
]);
}
return (
<main>
<h1>React оновлення</h1>
<Message count={count} />
<button onClick={() => setCount((currentCount) => currentCount + 1)}>
Збільшити лічильник
</button>
<button onClick={addTask}>
Додати завдання
</button>
<TaskList tasks={tasks} />
</main>
);
}Після натискання «Збільшити лічильник»:
змінюється state count;
App ререндериться;
Message отримує нове значення count;
TaskList теж може бути викликаний повторно;
React порівнює нове та попереднє дерева;
у DOM змінюється лише текст у Message.
Після натискання «Додати завдання»:
створюється новий масив tasks;
App ререндериться;
TaskList отримує новий список;
React знаходить новий елемент за його id;
до DOM додається лише новий пункт списку.
React визначає, чи було значення state оновлено, за посиланням для об’єктів і масивів. Тому state потрібно оновлювати, створюючи нове значення, а не змінюючи старе напряму.
Правильно:
setTasks((currentTasks) => [
...currentTasks,
newTask,
]);Тут створюється новий масив.
Неправильно:
tasks.push(newTask);
setTasks(tasks);У цьому випадку змінюється старий масив, а потім у state передається те саме посилання. Це може призвести до того, що React не визначить оновлення так, як очікується.
Для навчання зручно використовувати console.log у тілі компонента:
function Profile({ name }) {
console.log("Profile ререндериться");
return <p>{name}</p>;
}Також можна порівняти:
чи змінився state;
чи змінилися props;
чи ререндерився батьківський компонент;
чи змінився реальний DOM.
Не кожен ререндер є проблемою. Ререндер — звичайна частина роботи React. Проблемою він стає лише тоді, коли непотрібні ререндери спричиняють помітні затримки або зайві обчислення.
React спочатку виконує reconciliation і застосовує лише необхідні зміни. Повторний виклик компонента не означає повне перемальовування сторінки.
Не змінюйте state напряму через push, pop або присвоєння властивості об’єкта.
Замість цього створюйте нове значення:
setUser((currentUser) => ({
...currentUser,
name: "Олена",
}));Індекс може змінитися після вставки або видалення елемента. Для динамічних списків використовуйте стабільний id.
Не використовуйте такі ключі:
<li key={Math.random()}>{item.name}</li>Під час кожного рендеру ключ буде новим. React вважатиме всі елементи новими, видалятиме старі та створюватиме їх заново.
key використовується React для reconciliation і не передається компоненту як звичайний prop.
function Item(props) {
console.log(props.key); // undefined
return <li>{props.title}</li>;
}Якщо компоненту потрібен ідентифікатор, передайте його окремо:
<Item key={item.id} id={item.id} title={item.title} />Ререндер — це повторне виконання функції компонента та створення нового опису UI.
Ререндер може бути спричинений зміною state, props, контексту або ререндером батьківського компонента.
Ререндер не означає повну заміну DOM.
Reconciliation порівнює попереднє та нове дерево React-елементів.
React намагається застосувати мінімальний набір змін до DOM.
Однаковий тип елемента зазвичай дає змогу зберегти його DOM-вузол.
key допомагає React правильно зіставляти елементи списку.
Для динамічних списків використовуйте стабільні унікальні ідентифікатори, а не індекси чи випадкові значення.
State не слід змінювати напряму — для масивів і об’єктів потрібно створювати нове значення.