У світі React розвиток патернів проєктування є важливою складовою для створення підтримуваних і масштабованих додатків. Одним з найбільш ефективних патернів є Container/Presentational pattern, або патерн "Контейнер/Презентаційний компонент", також відомий як розподіл на "розумні" (Smart) і "тупі" (Dumb) компоненти. Цей патерн кардинально змінив підхід до структуризації компонентів у React і значно полегшив розробку.
Цей патерн передбачає поділ відповідальності між двома основними типами компонентів:
Презентаційні компоненти:
- Фокусуються тільки на відображенні інтерфейсу користувача (UI).
- Отримують дані через пропси.
- Рідко мають свій власний стан, за винятком стану UI.
- Пишуться як чисті функції, якщо це можливо.
- Не залежать від решти додатка (наприклад, від Redux або інших дій).
- Не визначають, як дані завантажуються чи змінюються.
Контейнерні компоненти:
- Фокусуються на логіці роботи.
- Подають дані та поведінку для презентаційних компонентів.
- Підключаються до джерел даних (наприклад, Redux, Context API, REST API).
- Мають стан і обробляють бізнес-логіку.
- Зазвичай є джерелами даних для презентаційних компонентів.
Переваги використання цього патерну:
- Розділення обов'язків: Розділяючи компоненти на дві категорії, ви робите ваш код більш організованим та зрозумілим.
- Покращена повторна використуваність: Презентаційні компоненти можна легко використовувати в різних частинах додатка, оскільки вони не залежать від конкретних джерел даних.
- Легше тестування: Коли компоненти мають одну відповідальність, тестування стає простішим. Презентаційні компоненти можна тестувати окремо, не мокуючи складну логіку завантаження даних.
- Покращена співпраця: Дизайнери можуть працювати над UI компонентами, а розробники — над логікою контейнерів, що дозволяє працювати паралельно.
- Адаптивність: Коли змінюється ваша структура даних (наприклад, ви переходите з Redux на Context API), потрібно оновити тільки контейнерні компоненти, залишаючи UI незмінним.
Практичний приклад: Створення функції профілю користувача
Розглянемо, як ми можемо структурувати функцію профілю користувача, використовуючи патерн Container/Presentational:
Презентаційний компонент:
// UserProfile.jsx
import React from 'react';
const UserProfile = ({ user, isLoading, error, onUpdateBio }) => {
if (isLoading) {
return
Loading user profile...
; } if (error) { return
Error loading profile: {error.message}
; } if (!user) { return
No user data available
; } return (
{user.name}
{user.bio}
onUpdateBio(e.target.value)} /> onUpdateBio(user.bio)}>Update Bio
); }; export default UserProfile;
- Отримує всі дані через пропси.
- Не займається завантаженням даних або управлінням складним станом.
- Лише відображає UI, використовуючи пропси.
- Делегує логіку через callback пропси (onUpdateBio).
Контейнерний компонент:
// UserProfileContainer.jsx
import React, { useState, useEffect } from 'react';
import UserProfile from './UserProfile';
const UserProfileContainer = ({ userId }) => {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchUserData = async () => {
setIsLoading(true);
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('Failed to fetch user data');
}
const userData = await response.json();
setUser(userData);
setError(null);
} catch (err) {
setError(err);
setUser(null);
} finally {
setIsLoading(false);
}
};
fetchUserData();
}, [userId]);
const handleUpdateBio = async (newBio) => {
try {
const response = await fetch(`/api/users/${userId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ bio: newBio }),
});
if (!response.ok) {
throw new Error('Failed to update bio');
}
setUser(prevUser => ({
...prevUser,
bio: newBio
}));
} catch (err) {
setError(err);
}
};
return (
<UserProfile user={user} isLoading={isLoading} error={error} onUpdateBio={handleUpdateBio} />
);
};
export default UserProfileContainer;
Цей контейнерний компонент:
- Керує логікою отримання даних.
- Зберігає стан.
- Обробляє API запити.
- Передає дані та колбеки до презентаційного компонента.
Використання Context API
Context API від React — це потужний інструмент для реалізації цього патерну, особливо для більш складних додатків:
// UserContext.js
import React, { createContext, useState, useContext, useEffect } from 'react';
const UserContext = createContext();
export const UserProvider = ({ children, userId }) => {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
// Логіка отримання даних та керування станом
useEffect(() => {
// ... отримання даних користувача
}, [userId]);
const updateBio = async (newBio) => {
// ...
};
return (
<UserContext.Provider value={{ user, isLoading, error, updateBio }}>
{children}
</UserContext.Provider>
);
};
export const useUser = () => useContext(UserContext);
Тепер ваші презентаційні компоненти можуть використовувати цей контекст:
// UserProfileView.jsx
import React from 'react';
import { useUser } from './UserContext';
const UserProfileView = () => {
const { user, isLoading, error, updateBio } = useUser();
// Відображення UI на основі даних з контексту
// ...
};
Підхід з власними хуками
Власні хуки змінили підхід до реалізації цього патерну. Вони дозволяють витягнути всю контейнерну логіку у багаторазові хуки:
// useUserProfile.js
import { useState, useEffect } from 'react';
export const useUserProfile = (userId) => {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// Логіка отримання даних
}, [userId]);
const updateBio = async (newBio) => {
// Логіка оновлення
};
return { user, isLoading, error, updateBio };
};
Тепер у вашому компоненті:
// UserProfile.jsx
import React from 'react';
import { useUserProfile } from './useUserProfile';
const UserProfile = ({ userId }) => {
const { user, isLoading, error, updateBio } = useUserProfile(userId);
// Відображення чистого UI на основі цих даних
// ...
};
Цей підхід зберігає розподіл обов'язків, роблячи контейнерну логіку більш багаторазовою та складною для комбінування.
Коли використовувати цей патерн
Патерн Container/Presentational найбільше підходить для таких ситуацій:
- Складні додатки: При створенні великих додатків з численними компонентами, що повинні ділитися даними.
- Спільна робота команди: Коли дизайнери UI та розробники додатків працюють паралельно.
- Повторне використання компонентів: Коли потрібно повторно використовувати UI компоненти з різними джерелами даних.
- Тестування: Коли ви хочете максимально підвищити тестованість ваших компонентів.
Загальні помилки, яких слід уникати
- Надмірна складність: Не кожен компонент повинен бути розділений. Для простих компонентів розділення може додати зайву складність.
- Жорстка прив'язаність: Занадто строгий підхід до цього патерну може призвести до зайвих абстракцій.
- Переміщення пропсів: Якщо ви маєте глибоко вкладені компоненти, може виникнути проблема з надмірним передаванням пропсів.
- Передчасна оптимізація: Не розділяйте компоненти, поки не з'явиться чітка потреба у повторному використанні або розподілі обов'язків.
Висновок
Патерн Container/Presentational залишається основним підходом у розробці React-додатків. Завдяки сучасним можливостям React, таким як хуки, цей патерн став більш гнучким і потужним, але принцип розподілу обов'язків залишається актуальним. Вправно застосовуючи цей патерн, можна створювати React-додатки, які легко підтримувати, тестувати та з якими зручно працювати в команді.
Перекладено з: Understanding the Container/Presentational Pattern in React