⚛️ 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.