Пошук уроків, статей та іншого контенту
Розділіть компоненти на контейнерні та презентаційні й визначте, де мають зберігатися дані та логіка відображення.
У React компонент часто виконує дві різні ролі:
контейнерний компонент зберігає дані та містить логіку;
презентаційний компонент відповідає за відображення інтерфейсу.
Такий поділ допомагає не змішувати отримання даних, зміну стану та JSX в одному великому компоненті.
Контейнерні та презентаційні компоненти — це не спеціальні типи компонентів React і не окремий API. Це підхід до організації коду.
Контейнерний компонент зазвичай:
зберігає стан за допомогою useState;
отримує або готує дані;
виконує обробники подій;
застосовує бізнес-логіку;
передає дані презентаційному компоненту через props.
Контейнер не обов’язково має повністю приховувати JSX. Його головне завдання — керувати даними та поведінкою.
Наприклад, контейнер може зберігати список завдань:
const [tasks, setTasks] = useState([
{ id: 1, title: 'Вивчити props', completed: true },
{ id: 2, title: 'Створити компонент', completed: false },
]);Також він може містити функцію зміни стану завдання:
function toggleTask(taskId) {
setTasks((currentTasks) =>
currentTasks.map((task) =>
task.id === taskId
? { ...task, completed: !task.completed }
: task
)
);
}Презентаційний компонент зазвичай:
отримує дані через props;
відображає розмітку;
викликає передані обробники подій;
не знає, звідки надійшли дані;
не відповідає за зберігання загального стану.
Такий компонент описує, як виглядає інтерфейс, а не як працюють дані.
function TaskList({ tasks, onToggle }) {
return (
<ul>
{tasks.map((task) => (
<li key={task.id}>
<span>{task.title}</span>
<button onClick={() => onToggle(task.id)}>
Змінити стан
</button>
</li>
))}
</ul>
);
}TaskList не змінює масив tasks самостійно. Він лише викликає onToggle, коли користувач натискає кнопку.
Дані в React зазвичай передаються зверху вниз:
контейнер зберігає стан;
контейнер передає стан презентаційному компоненту через props;
презентаційний компонент відображає дані;
після дії користувача презентаційний компонент викликає функцію з props;
контейнер змінює стан;
React повторно відображає компоненти з оновленими даними.
Схематично це виглядає так:
Container
├── tasks
├── toggleTask()
└── <TaskList tasks={tasks} onToggle={toggleTask} />
Presentation
├── відображає tasks
└── викликає onToggle()У цьому прикладі:
TaskListContainer — контейнерний компонент;
TaskList — презентаційний компонент;
список завдань і функція зміни стану знаходяться в контейнері;
розмітка списку знаходиться в презентаційному компоненті.
import { useState } from 'react';
function TaskList({ tasks, onToggle }) {
if (tasks.length === 0) {
return <p>Завдань немає.</p>;
}
return (
<ul>
{tasks.map((task) => (
<li key={task.id}>
<label>
<input
type="checkbox"
checked={task.completed}
onChange={() => onToggle(task.id)}
/>
<span
style={{
textDecoration: task.completed ? 'line-through' : 'none',
marginLeft: '8px',
}}
>
{task.title}
</span>
</label>
</li>
))}
</ul>
);
}
function TaskListContainer() {
const [tasks, setTasks] = useState([
{ id: 1, title: 'Вивчити props', completed: true },
{ id: 2, title: 'Створити контейнерний компонент', completed: false },
{ id: 3, title: 'Створити презентаційний компонент', completed: false },
]);
function toggleTask(taskId) {
setTasks((currentTasks) =>
currentTasks.map((task) =>
task.id === taskId
? { ...task, completed: !task.completed }
: task
)
);
}
const completedCount = tasks.filter((task) => task.completed).length;
return (
<section>
<h1>Список завдань</h1>
<p>
Виконано: {completedCount} з {tasks.length}
</p>
<TaskList tasks={tasks} onToggle={toggleTask} />
</section>
);
}
export default function App() {
return <TaskListContainer />;
}TaskListContainer:
створює стан tasks;
містить функцію toggleTask;
обчислює кількість виконаних завдань;
передає tasks і toggleTask компоненту TaskList.
TaskList:
отримує tasks та onToggle через props;
перебирає завдання за допомогою map;
відображає прапорець і назву кожного завдання;
викликає onToggle(task.id) після зміни прапорця.
Презентаційний компонент не знає, що завдання зберігаються в useState. У майбутньому контейнер може отримувати їх із сервера, але TaskList не обов’язково доведеться змінювати.
У контейнері розміщують стан, який потрібен для роботи компонента або його дочірніх компонентів:
const [tasks, setTasks] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [filter, setFilter] = useState('all');Також у контейнері доречно розмістити:
підготовку даних для відображення;
фільтрацію та сортування;
обробку натискання кнопок;
зміну стану;
логіку завантаження даних.
Наприклад:
const visibleTasks = tasks.filter((task) => {
if (filter === 'completed') {
return task.completed;
}
if (filter === 'active') {
return !task.completed;
}
return true;
});Після цього контейнер передає вже підготовлений список:
<TaskList tasks={visibleTasks} onToggle={toggleTask} />У презентаційному компоненті зазвичай залишають:
JSX;
класи або стилі;
відображення різних станів інтерфейсу;
виклик обробників, отриманих через props.
Наприклад, компонент може показати повідомлення, якщо список порожній:
function TaskList({ tasks }) {
if (tasks.length === 0) {
return <p>Завдань немає.</p>;
}
return (
<ul>
{tasks.map((task) => (
<li key={task.id}>{task.title}</li>
))}
</ul>
);
}Це логіка відображення: компонент вирішує, який JSX показати для отриманих даних.
Презентаційний компонент може мати невеликий локальний стан, якщо цей стан стосується лише його інтерфейсу.
Наприклад, стан відкриття меню або видимості підказки:
import { useState } from 'react';
function HelpButton() {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>
Показати довідку
</button>
{isOpen && <p>Оберіть завдання зі списку.</p>}
</div>
);
}Це не суперечить поділу на ролі. Стан isOpen потрібен лише для внутрішнього вигляду HelpButton, тому немає потреби переносити його в контейнер.
Важливо відрізняти:
стан даних застосунку — список завдань, користувач, результати запиту;
локальний стан інтерфейсу — відкрито чи закрито меню, активна вкладка, видимість підказки.
Один контейнер може керувати даними та передавати їх кільком компонентам:
function TaskListContainer() {
const [tasks, setTasks] = useState([]);
return (
<>
<TaskSummary tasks={tasks} />
<TaskList tasks={tasks} onToggle={toggleTask} />
</>
);
}У такому випадку TaskSummary відповідає за підсумок, а TaskList — за список. Обидва компоненти отримують лише ті дані, які їм потрібні.
Розділення особливо корисне, якщо компонент:
став занадто великим;
одночасно завантажує дані та містить складну розмітку;
має багато обробників подій;
складно тестується;
містить частину інтерфейсу, яку можна використати повторно.
Початковий компонент може виглядати так:
function Tasks() {
const [tasks, setTasks] = useState([]);
function toggleTask(taskId) {
// Зміна стану завдання
}
return (
<section>
<h1>Завдання</h1>
{tasks.map((task) => (
<div key={task.id}>
<span>{task.title}</span>
<button onClick={() => toggleTask(task.id)}>
Змінити
</button>
</div>
))}
</section>
);
}Після розділення:
function TasksContainer() {
const [tasks, setTasks] = useState([]);
function toggleTask(taskId) {
// Зміна стану завдання
}
return (
<section>
<h1>Завдання</h1>
<TaskList tasks={tasks} onToggle={toggleTask} />
</section>
);
}
function TaskList({ tasks, onToggle }) {
return (
<div>
{tasks.map((task) => (
<div key={task.id}>
<span>{task.title}</span>
<button onClick={() => onToggle(task.id)}>
Змінити
</button>
</div>
))}
</div>
);
}Тепер кожен компонент має зрозумілішу відповідальність.
Ні. Для маленького компонента додаткове розділення може лише ускладнити код.
Наприклад, якщо компонент має кілька рядків і не містить складної логіки, його не обов’язково розділяти на контейнер і презентаційний компонент.
Поділ варто застосовувати тоді, коли він покращує:
читабельність;
повторне використання;
тестування;
підтримку коду.
У сучасному React контейнерні та презентаційні компоненти — це гнучкий патерн, а не суворе правило. Один компонент може частково виконувати обидві ролі.
Якщо контейнер і презентаційний компонент мають окремі копії одного списку, дані можуть розсинхронізуватися.
Краще зберігати спільний стан в одному компоненті та передавати його вниз через props.
Props доступні лише для читання. Не слід змінювати їх напряму:
// Неправильно
task.completed = !task.completed;Зміну має виконувати контейнер через функцію оновлення стану:
setTasks((currentTasks) =>
currentTasks.map((task) =>
task.id === taskId
? { ...task, completed: !task.completed }
: task
)
);Якщо презентаційний компонент сам фільтрує дані, змінює стан і виконує кілька різних операцій, його роль стає нечіткою.
Краще підготувати дані в контейнері та передати компоненту готові значення й обробники.
Компонент списку не повинен знати, чи дані:
створені в useState;
отримані з форми;
завантажені із сервера;
передані з іншого компонента.
Чим менше компонент знає про джерело даних, тим легше його повторно використати.
Не кожна кнопка або текстовий блок потребує окремого контейнера. Розділяйте компоненти тоді, коли це справді спрощує код.
Контейнерний компонент відповідає за стан, дані та логіку.
Презентаційний компонент відповідає за відображення та взаємодію з отриманими props.
Контейнер передає дані вниз, а презентаційний компонент повідомляє контейнер про дії користувача через callback-функції.
Дані не слід змінювати безпосередньо через props.
Локальний стан інтерфейсу може залишатися в презентаційному компоненті.
Це гнучкий патерн організації коду, а не обов’язкова вимога React.