Розуміння патерну Container/Presentational у React

У світі React розвиток патернів проєктування є важливою складовою для створення підтримуваних і масштабованих додатків. Одним з найбільш ефективних патернів є Container/Presentational pattern, або патерн "Контейнер/Презентаційний компонент", також відомий як розподіл на "розумні" (Smart) і "тупі" (Dumb) компоненти. Цей патерн кардинально змінив підхід до структуризації компонентів у React і значно полегшив розробку.

Цей патерн передбачає поділ відповідальності між двома основними типами компонентів:

Презентаційні компоненти:

  • Фокусуються тільки на відображенні інтерфейсу користувача (UI).
  • Отримують дані через пропси.
  • Рідко мають свій власний стан, за винятком стану UI.
  • Пишуться як чисті функції, якщо це можливо.
  • Не залежать від решти додатка (наприклад, від Redux або інших дій).
  • Не визначають, як дані завантажуються чи змінюються.

Контейнерні компоненти:

  • Фокусуються на логіці роботи.
  • Подають дані та поведінку для презентаційних компонентів.
  • Підключаються до джерел даних (наприклад, Redux, Context API, REST API).
  • Мають стан і обробляють бізнес-логіку.
  • Зазвичай є джерелами даних для презентаційних компонентів.

Переваги використання цього патерну:

  1. Розділення обов'язків: Розділяючи компоненти на дві категорії, ви робите ваш код більш організованим та зрозумілим.
  2. Покращена повторна використуваність: Презентаційні компоненти можна легко використовувати в різних частинах додатка, оскільки вони не залежать від конкретних джерел даних.
  3. Легше тестування: Коли компоненти мають одну відповідальність, тестування стає простішим. Презентаційні компоненти можна тестувати окремо, не мокуючи складну логіку завантаження даних.
  4. Покращена співпраця: Дизайнери можуть працювати над UI компонентами, а розробники — над логікою контейнерів, що дозволяє працювати паралельно.
  5. Адаптивність: Коли змінюється ваша структура даних (наприклад, ви переходите з 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 компоненти з різними джерелами даних.
  • Тестування: Коли ви хочете максимально підвищити тестованість ваших компонентів.

Загальні помилки, яких слід уникати

  1. Надмірна складність: Не кожен компонент повинен бути розділений. Для простих компонентів розділення може додати зайву складність.
  2. Жорстка прив'язаність: Занадто строгий підхід до цього патерну може призвести до зайвих абстракцій.
  3. Переміщення пропсів: Якщо ви маєте глибоко вкладені компоненти, може виникнути проблема з надмірним передаванням пропсів.
  4. Передчасна оптимізація: Не розділяйте компоненти, поки не з'явиться чітка потреба у повторному використанні або розподілі обов'язків.

Висновок

Патерн Container/Presentational залишається основним підходом у розробці React-додатків. Завдяки сучасним можливостям React, таким як хуки, цей патерн став більш гнучким і потужним, але принцип розподілу обов'язків залишається актуальним. Вправно застосовуючи цей патерн, можна створювати React-додатки, які легко підтримувати, тестувати та з якими зручно працювати в команді.

Перекладено з: Understanding the Container/Presentational Pattern in React