⚛️ 6 самых важных шаблонов проектирования в React

Правильное использование паттернов проектирования – ключ к созданию надежных, эффективных и масштабируемых React-приложений. Рассказываем о шаблонах, которые стоит освоить в первую очередь.

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

  • организации компонентов;
  • управлению состоянием приложения;
  • обработке данных.

Использование шаблонов улучшает производительность кода, облегчает поддержку и масштабирование проектов. В этой статье мы рассмотрим наиболее важные и распространенные шаблоны проектирования в React:

  • Контейнер и презентация
  • Компонент высшего порядка
  • Составные компоненты
  • Провайдер
  • Редуктор состояния
  • Компоновщик

Контейнер и презентация

Этот паттерн разделяет компоненты на две категории: контейнеры и презентационные компоненты:

  • Контейнеры управляют данными и логикой состояния. Они загружают данные из внешних источников, при необходимости их обрабатывают и передают в компоненты презентации через пропсы. Контейнеры часто связаны с внешними сервисами, хранилищами Redux или контекстными провайдерами.
  • Презентационные компоненты сосредоточены исключительно на отображении элементов интерфейса. Они получают данные от контейнеров через пропсы и визуализируют их в соответствии с HTML-разметкой и стилями CSS. Обычно это статические функциональные компоненты, которые проще тестировать и переиспользовать.

В качестве примера можно представить панель управления соцсетью, где пользователи просматривают посты друзей и взаимодействуют с ними. Вот как можно структурировать компоненты:

Контейнерный компонент FriendFeedContainer отвечает за получение данных о постах друзей из API, выполнение любых манипуляций с этими данными и управление состоянием ленты новостей. Затем он передает данные в соответствующие презентационные компоненты.

import React, { useState, useEffect } from 'react';
import FriendFeed from './FriendFeed';

const FriendFeedContainer = () => {
  const [friendPosts, setFriendPosts] = useState([]);

  useEffect(() => {
    // Получение постов друзей из API
    const fetchFriendPosts = async () => {
      const posts = await fetch('https://api.example.com/friend-posts');
      const data = await posts.json();
      setFriendPosts(data);
    };
    fetchFriendPosts();
  }, []);

  return <FriendFeed posts={friendPosts} />;
};

export default FriendFeedContainer;

Презентационный компонент FriendFeed отвечает за отображение постов друзей на экране. Он получает данные о постах от своего родительского контейнерного компонента FriendFeedContainer через пропсы.

Получив данные, FriendFeed форматирует текст, добавляет изображения, аватары пользователей и любые другие визуальные составляющие постов. По сути, этот компонент не знает, откуда берутся данные, он просто их отображает в привлекательном виде.

import React from 'react';

const FriendFeed = ({ posts }) => {
  return (
    <div>
      <h2>Friend Feed</h2>
      <ul>
        {posts.map(post => (
          <li key={post.id}>
            <p>{post.content}</p>
            <p>Posted by: {post.author}</p>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default FriendFeed;
👨‍💻🎨 Библиотека фронтендера
Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека фронтендера»

Компонент высшего порядка

Компонент высшего порядка (HOC – Higher-Order Component) – это способ повторного использования логики компонентов в React. HOC представляет собой функцию, которая принимает компонент в качестве аргумента и возвращает новый компонент с дополнительной функциональностью.

Возьмем пример панели управления соцсетью, созданной с помощью хуков. Представим, что у нас есть несколько компонентов, которым нужно получать данные о пользователях из API. Вместо того чтобы дублировать код получения данных в каждом компоненте, можно создать HOC для получения данных и передачи их обернутым компонентам в качестве пропсов:

import React, { useState, useEffect } from 'react';

// Определение компонента высшего порядка для получения данных
const withUserData = (WrappedComponent) => {
  return (props) => {
    const [userData, setUserData] = useState(null);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
      // Получение данных о пользователях из API
      const fetchData = async () => {
        try {
          const response = await fetch('https://api.example.com/user');
          const data = await response.json();
          setUserData(data);
          setLoading(false);
        } catch (error) {
          console.error('Error fetching user data:', error);
          setLoading(false);
        }
      };

      fetchData();
    }, []);

    return (
      <div>
        {loading ? (
          <p>Loading...</p>
        ) : (
          <WrappedComponent {...props} userData={userData} />
        )}
      </div>
    );
  };
};

// Создание компонента для отображения данных
const UserProfile = ({ userData }) => {
  return (
    <div>
      <h2>User Profile</h2>
      {userData && (
        <div>
          <p>Name: {userData.name}</p>
          <p>Email: {userData.email}</p>
          {/* Additional user data fields */}
        </div>
      )}
    </div>
  );
};

// Оборачивание компонента UserProfile в HOC withUserData 
const UserProfileWithUserData = withUserData(UserProfile);

// Основной компонент, в котором можно рендерить содержимое обернутых компонентов
const SocialMediaDashboard = () => {
  return (
    <div>
      <h1>Social Media Dashboard</h1>
      <UserProfileWithUserData />
    </div>
  );
};

export default SocialMediaDashboard;

Этот паттерн позволяет повторно использовать логику получения данных во всех компонентах панели управления социальной сетью, не дублируя код:

  • withUserData – компонент высшего порядка, который отвечает за получение пользовательских данных из API. Он оборачивает переданный компонент WrappedComponent и предоставляет ему полученные данные пользователя в качестве пропсов userData.
  • UserProfile – функциональный компонент, который принимает пропсы userData и отображает информацию о профиле пользователя.
  • UserProfileWithUserData – компонент, полученный путем обертывания UserProfile с помощью withUserData. Он наследует функциональность получения данных о пользователе от withUserData и отрисовывает профиль пользователя с помощью UserProfile.
  • SocialMediaDashboard – основной компонент, в котором можно рендерить UserProfileWithUserData или любой другой компонент, которому нужны данные пользователя.
***

Хочу освоить современный стек и работать фронтенд-разработчиком. Что посоветуете?

Заглядывай на наш курс «Frontend Basic: принцип работы современного веба. С нуля до первого интернет-магазина» на котором ты освоишь HTML, CSS, JavaScript, React, Git. Курс состоит из 26 уроков и 28 домашних заданий. Наставник дает обратную связь. Для тех, кто хочет погрузиться в среду более углубленно, мы подготовили тариф с личным наставником.

***

Составные компоненты

Этот паттерн позволяет создавать компоненты, которые вместе формируют единый пользовательский интерфейс. При этом он обеспечивает четкое разделение ответственности и гибкость в настройке поведения и внешнего вида компонентов.

В этом шаблоне родительский компонент выступает контейнером для одного или нескольких дочерних компонентов, называемых составными компонентами. Эти дочерние компоненты взаимодействуют друг с другом для достижения определенной функциональности. Главная особенность составных компонентов заключается в том, что они совместно используют состояние и функциональность через свой родительский компонент. Такой подход упрощает разработку и помогает поддерживать чистоту кода.

Вот простой пример реализации паттерна составных компонентов в React с использованием хуков:

import React, { useState } from 'react';

// Родительский компонент, содержащий составные компоненты
const Toggle = ({ children }) => {
  const [isOn, setIsOn] = useState(false);

  // Функция переключения состояния
  const toggle = () => {
    setIsOn((prevIsOn) => !prevIsOn);
  };

  // Клонирование дочерних компонентов и передача им функции переключения и состояния
  const childrenWithProps = React.Children.map(children, (child) => {
    if (React.isValidElement(child)) {
      return React.cloneElement(child, { isOn, toggle });
    }
    return child;
  });

  return <div>{childrenWithProps}</div>;
};

// Дочерний компонент для кнопки переключения
const ToggleButton = ({ isOn, toggle }) => {
  return (
    <button onClick={toggle}>
      {isOn ? 'Turn Off' : 'Turn On'}
    </button>
  );
};

// Дочерний компонент для статуса переключения
const ToggleStatus = ({ isOn }) => {
  return <p>The toggle is {isOn ? 'on' : 'off'}.</p>;
};

// Основной компонент, в котором используются составные компоненты
const App = () => {
  return (
    <Toggle>
      <ToggleStatus />
      <ToggleButton />
    </Toggle>
  );
};

export default App;

В этом примере:

  • Toggle – родительский компонент, который объединяет составные компоненты ToggleButton и ToggleStatus.
  • ToggleButton – дочерний компонент, отвечающий за отрисовку кнопки переключения.
  • ToggleStatus – другой дочерний компонент, отвечающий за отображение состояния переключения.
  • Компонент Toggle управляет состоянием isOn и предоставляет функцию toggle для его изменения. Он клонирует свои дочерние компоненты ToggleButton и ToggleStatus и передает им свое состояние isOn и функцию toggle в качестве пропсов.

Провайдер

Паттерн провайдер в React используется для управления и совместного использования состояния или данных приложения несколькими компонентами. Он подразумевает создание компонента-провайдера, который инкапсулирует состояние или данные и предоставляет их своим дочерним компонентам через контекст (context API). Такой подход позволяет обойтись без проброса пропсов и упрощает доступ к данным из любой части компонентного дерева.

Рассмотрим пример использования паттерна провайдера в React для управления данными пользователя во время авторизации:

// UserContext.js
import React, { createContext, useState } from 'react';

// Создание контекста для данных пользователя
const UserContext = createContext();

// Компонент провайдера
export const UserProvider = ({ children }) => {
  const [user, setUser] = useState(null);

  // Функция логина
  const login = (userData) => {
    setUser(userData);
  };

  // Функция выхода
  const logout = () => {
    setUser(null);
  };

  return (
    <UserContext.Provider value={{ user, login, logout }}>
      {children}
    </UserContext.Provider>
  );
};

export default UserContext;

В этом примере мы:

  • Создаем контекст с названием UserContext при помощи функции createContext. Этот контекст будет использоваться для передачи данных пользователя и функций, связанных с авторизацией, между компонентами.
  • Определяем компонент UserProvider, который служит провайдером для UserContext. Этот компонент управляет состоянием пользователя с помощью хука useState и предоставляет методы login и logout для обновления этого состояния.
  • Внутри UserProvider оборачиваем дочерние компоненты с помощью UserContext.Provider и передаем состояние пользователя user и функции login и logout в качестве значений. Теперь любой компонент, которому нужен доступ к данным пользователя или функциям, связанным с авторизацией, может использовать UserContext с помощью хука useContext.

Рассмотрим пример компонента, который получает данные пользователя из контекста:

// UserProfile.js
import React, { useContext } from 'react';
import UserContext from './UserContext';

const UserProfile = () => {
  const { user, logout } = useContext(UserContext);

  return (
    <div>
      {user ? (
        <div>
          <h2>Welcome, {user.username}!</h2>
          <button onClick={logout}>Logout</button>
        </div>
      ) : (
        <div>
          <h2>Please log in</h2>
        </div>
      )}
    </div>
  );
};

export default UserProfile;

В этом компоненте мы:

  • Импортируем UserContext и используем хук useContext для доступа к данным пользователя user и функции logout, предоставляемым UserProvider.
  • В зависимости от того, авторизован ли пользователь (loggedIn), отрисовываем разные элементы интерфейса. Например, если пользователь авторизован (loggedIn имеет значение true), можно отобразить приветственное сообщение и кнопку выхода logout, а если не авторизован (loggedIn имеет значение false), можно показать кнопку входа.
// App.js
import React from 'react';
import { UserProvider } from './UserContext';
import UserProfile from './UserProfile';

const App = () => {
  return (
    <UserProvider>
      <div>
        <h1>My App</h1>
        <UserProfile />
      </div>
    </UserProvider>
  );
};

export default App;

Редуктор состояния

Паттерн редуктор состояния – это контролируемый и предсказуемый способ управления состоянием React-приложения. Он использует функцию-редуктор для обработки изменений состояния и действий – точно так же, как это делают редукторы в Redux. Этот шаблон особенно полезен для управления сложной логикой состояния или состоянием, которое необходимо совместно использовать несколькими компонентами.

Рассмотрим пример реализации паттерна редуктора состояния для управления публикациями и уведомлениями пользователя в соцсети:

import React, { useReducer } from 'react';

// Типы действий
const ADD_POST = 'ADD_POST';
const DELETE_POST = 'DELETE_POST';
const ADD_NOTIFICATION = 'ADD_NOTIFICATION';
const DELETE_NOTIFICATION = 'DELETE_NOTIFICATION';

// Редуктор
const dashboardReducer = (state, action) => {
  switch (action.type) {
    case ADD_POST:
      return { ...state, posts: [...state.posts, action.payload] };
    case DELETE_POST:
      return { ...state, posts: state.posts.filter(post => post.id !== action.payload.id) };
    case ADD_NOTIFICATION:
      return { ...state, notifications: [...state.notifications, action.payload] };
    case DELETE_NOTIFICATION:
      return { ...state, notifications: state.notifications.filter(notification => notification.id !== action.payload.id) };
    default:
      return state;
  }
};

// Компонент панели управления
const Dashboard = () => {
  // Initialize state using useReducer hook
  const [state, dispatch] = useReducer(dashboardReducer, { posts: [], notifications: [] });

  // Функция добавления поста
  const addPost = (text) => {
    const newPost = { id: Date.now(), text };
    dispatch({ type: ADD_POST, payload: newPost });
  };

  // Функция удаления поста
  const deletePost = (id) => {
    dispatch({ type: DELETE_POST, payload: { id } });
  };

  // Функция уведомления
  const addNotification = (text) => {
    const newNotification = { id: Date.now(), text };
    dispatch({ type: ADD_NOTIFICATION, payload: newNotification });
  };

  // Функция удаления
  const deleteNotification = (id) => {
    dispatch({ type: DELETE_NOTIFICATION, payload: { id } });
  };

  return (
    <div>
      <h1>Social Media Dashboard</h1>
      <div>
        <h2>Posts</h2>
        <ul>
          {state.posts.map(post => (
            <li key={post.id}>
              {post.text}
              <button onClick={() => deletePost(post.id)}>Delete</button>
            </li>
          ))}
        </ul>
        <button onClick={() => addPost('New post')}>Add Post</button>
      </div>
      <div>
        <h2>Notifications</h2>
        <ul>
          {state.notifications.map(notification => (
            <li key={notification.id}>
              {notification.text}
              <button onClick={() => deleteNotification(notification.id)}>Dismiss</button>
            </li>
          ))}
        </ul>
        <button onClick={() => addNotification('New notification')}>Add Notification</button>
      </div>
    </div>
  );
};

export default Dashboard;

В этом примере мы:

  • Определяем ADD_POST, DELETE_POST, ADD_NOTIFICATION, DELETE_NOTIFICATION – действия, которые можно выполнять с данными в панели управления аккаунта.
  • Определяем функцию-редуктор dashboardReducer, которая принимает текущее состояние и действие, а затем возвращает новое состояние на основе этого действия.
  • С помощью хука useReducer создаем значение состояния state и функцию dispatch для обновления состояния путем передачи действий редуктору.
  • Компонент Dashboard содержит логику для добавления и удаления постов и уведомлений. Он вызывает dispatch с определенными действиями, чтобы обновить состояние соответствующим образом.
  • В интерфейсе посты и уведомления отображаются как элементы списка с кнопками для их удаления. Пользователи могут добавлять новые посты или уведомления, нажимая соответствующие кнопки.

Как очевидно из этого примера, функция-редуктор централизует логику управления состоянием, делая ее более понятной и поддерживаемой.

Компоновщик

Идея компоновщика заключается в объединении мелких, многоразовых компонентов для создания более сложных компонентов или интерфейсов. Давайте проиллюстрируем этот паттерн на примере панели управления в соцсети.

Предположим, эта панель управления состоит из нескольких компонентов – UserInfo (информация о пользователе), Feed (лента новостей), Notifications (уведомления) и Sidebar (боковая панель). Можно объединить эти мелкие компоненты вместе, чтобы создать единый компонент панели управления. Такой компонент панели управления может выглядеть следующим образом:

import React from 'react';

// информация о пользователе
const UserInfo = ({ user }) => {
  return (
    <div className="user-info">
      <img src={user.profilePic} alt="Profile" />
      <h3>{user.name}</h3>
    </div>
  );
};

// лента новостей
const Feed = ({ posts }) => {
  return (
    <div className="feed">
      <h2>Feed</h2>
      <ul>
        {posts.map((post, index) => (
          <li key={index}>
            <h4>{post.title}</h4>
            <p>{post.content}</p>
          </li>
        ))}
      </ul>
    </div>
  );
};

// уведомления
const Notifications = ({ notifications }) => {
  return (
    <div className="notifications">
      <h2>Notifications</h2>
      <ul>
        {notifications.map((notification, index) => (
          <li key={index}>
            <p>{notification}</p>
          </li>
        ))}
      </ul>
    </div>
  );
};

// боковая панель
const Sidebar = () => {
  return (
    <div className="sidebar">
      <ul>
        <li>Home</li>
        <li>Profile</li>
        <li>Messages</li>
        <li>Settings</li>
      </ul>
    </div>
  );
};

// панель управления, состоящая из мелких компонентов
const SocialMediaDashboard = ({ user, posts, notifications }) => {
  return (
    <div className="social-media-dashboard">
      <UserInfo user={user} />
      <div className="main-content">
        <Feed posts={posts} />
        <Notifications notifications={notifications} />
      </div>
      <Sidebar />
    </div>
  );
};

// главный компонент
const App = () => {
  // Sample data
  const user = {
    name: "John Doe",
    profilePic: "https://via.placeholder.com/150",
  };

  const posts = [
    { title: "Post 1", content: "Content of post 1" },
    { title: "Post 2", content: "Content of post 2" },
    { title: "Post 3", content: "Content of post 3" },
  ];

  const notifications = [
    "You have a new friend request",
    "Your post has been liked",
    "You have a new message",
  ];

  return (
    <div>
      <h1>Social Media Dashboard</h1>
      <SocialMediaDashboard user={user} posts={posts} notifications={notifications} />
    </div>
  );
};

export default App;

В этом примере мы:

  • Используем отдельные компоненты для UserInfo, Feed, Notifications и Sidebar. Каждый из них отвечает за отрисовку определенной части панели управления социальной сетью.
  • Объединяем эти мелкие компоненты вместе в SocialMediaDashboard, чтобы создать весь интерфейс панели управления.
  • Компонент App является точкой входа, где происходит передача данных (информации о пользователе, постов, уведомлений) в компонент SocialMediaDashboard.

Каждый компонент фокусируется на определенном аспекте интерфейса – такой код легко переиспользовать и поддерживать.

Другие полезные паттерны

Набор шаблонов, которые можно использовать при разработке React-приложений, не ограничивается перечисленными выше паттернами. Более полная коллекция паттернов представлена в репозитории книги React 18 Design Patterns and Best Practices, бесплатную версию которой можно скачать здесь.

***

При подготовке статьи использовалась публикация React Component Design Patterns.

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ