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

Правильное использование паттернов проектирования – ключ к созданию надежных, эффективных и масштабируемых React-приложений. Рассказываем о шаблонах, которые стоит освоить в первую очередь.
⚛️ 6 самых важных шаблонов проектирования в 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.

МЕРОПРИЯТИЯ

Комментарии

ВАКАНСИИ

Добавить вакансию
Java Engineer
по итогам собеседования
Senior Java Developer
от 350000 RUB до 400000 RUB

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