Разделение ответственности в React. Как использовать контейнерные и презентационные компоненты.
2 года назад·6 мин. на чтение
Многие новички в React объединяют логику и представление в одном и том же компоненте. И они могут не знать, почему важно разделять эти две вещи.
В таком случае может обнаружиться, что нужно внести большие изменения в файл. Затем придется вносить много переделок, чтобы разделить логику и представление.
Это происходит из-за того, что разработчик может не знать о разделении ответственности и таком шаблоне как презентационные и контейнерные компоненты (presentational and container components). В этой статье рассмотрим этот паттерн, чтобы смягчить эту проблему на ранних этапах жизненного цикла разработки проекта.
Чтобы решить эту проблему и придерживаться разделения ответственности, мы должны разделить эти две части — то есть запрос данных и их представление в пользовательском интерфейсе — на два разных компонента.
Шаблон контейнеры и презентационные компоненты (smart/dummy components) поможет нам решить эту проблему.
Вот что делает этот компонент:
Ниже приведены некоторые причины, по которым нам требуются контейнерные и презентационные компоненты:
Компонент
Файл с типами.
Компонент
Компонент
Теперь мы можем просто удалить компонент-контейнер
Что такое разделение ответственности?
Разделение ответственности — это концепция, которая широко используется в программировании. В нем говорится, что логика, выполняющая разные действия, не должна группироваться или объединяться вместе. Например, то, что мы обсуждали во вводной части, нарушает разделение задач, потому что мы поместили логику выборки данных и представления данных в один и тот же компонент.Что такое контейнерные и презентационные компоненты?
Контейнерные компоненты
Это компоненты, которые предоставляют, создают или хранят данные для дочерних компонентов. Единственная работа компонента-контейнера — обработка данных. Он не состоит из собственного пользовательского интерфейса. Скорее, он состоит из презентационных компонентов в качестве своих дочерних элементов, которые используют эти данные. Простым примером может быть компонент с именемFetchUserContainer
, который состоит из некоторой логики, которая извлекает данные всех пользователей.
Презентационные компоненты
Это компоненты, основной обязанностью которых является представление данных в пользовательском интерфейсе. Они принимают данные из компонентов контейнера. Эти компоненты не имеют состояния, если только им не требуется собственное состояние для отображения пользовательского интерфейса. Они не изменяют данные, которые они получают. Примером этого может быть компонентUserList
, который просто отображает всех пользователей.
Зачем нам нужны эти компоненты?
Чтобы понять это, возьмем простой пример. Мы хотим отобразить список сообщений, которые мы получаем из JSON placeholder API.// DisplayPosts.tsx import { useEffect, useState } from "react"; interface ISinglePost { userId: number; id: number; title: string; body: string; } /* Пример того как НЕ нужно объединять логику и отображение */ export default function DisplayPosts() { const [posts, setPosts] = useState<ISinglePost[] | null>(null); const [isLoading, setIsLoading] = useState<Boolean>(false); const [error, setError] = useState<unknown>(); // Логика useEffect(() => { (async () => { try { setIsLoading(true); const resp = await fetch("https://jsonplaceholder.typicode.com/posts"); const data = await resp.json(); setPosts(data); setIsLoading(false); } catch (err) { setError(err); setIsLoading(false); } })(); }, []); // Представление return isLoading ? ( <span>Loading... </span> ) : posts ? ( <ul> {posts.map((post: ISinglePost) => ( <li key={`item-${post.id}`}> <span>{post.title}</span> </li> ))} </ul> ) : ( <span>{JSON.stringify(error)}</span> ); }
- Он имеет 3 переменные состояния:
posts
,isLoading
иerror
. - У нас есть хук
useEffect
, который состоит из бизнес-логики. Здесь мы извлекаем данные из jsonplaceholder API с помощьюfetch
. - Когда данные извлекаются, мы сохраняем их в переменной состояния
posts
, используяsetPosts
. - Мы также гарантируем, что переключаем значения
isLoading
иerror
во время соответствующих сценариев. - Мы поместили всю эту логику в асинхронную функцию.
- Возвращаем посты в виде списка и отображаем их.
- Они помогают нам создавать слабосвязанные компоненты.
- Они помогают нам поддерживать разделение ответственности.
- Рефакторинг кода становится намного проще.
- Код становится более организованным и удобным в сопровождении
- Это значительно упрощает тестирование.
Пример компонента-представления и контейнера
Для примера будем использовать тот же пример, что и выше — получение данных из JSON placeholder API. Разберемся со структурой файлов. Нашим контейнерным компонентом будетPostContainer
. У нас будет два презентационных компонента:
Posts
: компонент с неупорядоченным списком.SinglePost
: компонент, отображающий элемент списка.
components
. Теперь, когда мы знаем, что куда помещать, давайте начнем с компонента-контейнера: PostContainer
.
Компонент PostContainer
// components/PostContainer.tsx import { useEffect, useState } from "react"; import { ISinglePost } from "../Definitions"; import Posts from "./Posts"; export default function PostContainer() { const [posts, setPosts] = useState<ISinglePost[] | null>(null); const [isLoading, setIsLoading] = useState<Boolean>(false); const [error, setError] = useState<unknown>(); useEffect(() => { (async () => { try { setIsLoading(true); const resp = await fetch("https://jsonplaceholder.typicode.com/posts"); const data = await resp.json(); setPosts(data.filter((post: ISinglePost) => post.userId === 1)); setIsLoading(false); } catch (err) { setError(err); setIsLoading(false); } })(); }, []); return isLoading ? ( <span>Loading... </span> ) : posts ? ( <Posts posts={posts} /> ) : ( <span>{JSON.stringify(error)}</span> ); }
Приведенный выше код просто содержит логику выборки данных. Эта логика присутствует в хукеcomponents/Definitions.ts export interface SinglePost { userId: number; id: number; title: string; body: string; }
useEffect
. Этот компонент-контейнер передает эти данные презентационному компоненту Posts
.
Давайте взглянем на презентационный компонент Posts
.
Компонент Posts
Как видите, это простой файл, состоящий из тега// components/Posts.tsx import { ISinglePost } from "../Definitions"; import SinglePost from "./SinglePost"; export default function Posts(props: { posts: ISinglePost[] }) { return ( <ul style={{ display: "flex", flexDirection: "column", alignItems: "center" }} > {props.posts.map((post: ISinglePost) => ( <SinglePost {...post} /> ))} </ul> ); }
ul
— неупорядоченного списка. Затем этот компонент отображает посты, которые передаются в качестве пропса. Мы передаем каждый объект поста в компонент SinglePost
.
Существует еще один презентационный компонент, который отображает элемент списка, это тег li
. Он отображает заголовок и тело сообщения.
Компонент SinglePost
Эти презентационные компоненты просто отображают данные на экране. Вот и все. Они не делают ничего другого. Поскольку здесь они просто отображают данные, они также будут иметь собственные стили. Теперь, когда мы настроили компоненты, давайте посмотрим, что удалось достичь:// components/SinglePost.tsx import { ISinglePost } from "../Definitions"; export default function SinglePost(props: ISinglePost) { const { userId, id, title, body } = props; return ( <li key={`item-${userId}-${id}`} style={{ width: 400 }}> <h4> <strong>{title}</strong> </h4> <span>{body}</span> </li> ); }
- Концепция разделения ответственности в этом примере не нарушается.
- Написание модульных тестов для каждого компонента становится проще.
- Сопровождаемость и читабельность кода намного лучше. Таким образом, наша кодовая база стала намного более организованной.
Как заменить контейнерные компоненты на React хуки
Начиная с React 16.8 стало намного проще создавать и разрабатывать компоненты с помощью функциональных компонентов и хуков. Здесь мы воспользуемся этими возможностями и заменим компонент-контейнер хуком.Что дает это улучшение:// hooks/usePosts.ts import { useEffect, useState } from "react"; import { ISinglePost } from "../Definitions"; export default function usePosts() { const [posts, setPosts] = useState<ISinglePost[] | null>(null); const [isLoading, setIsLoading] = useState<Boolean>(false); const [error, setError] = useState<unknown>(); useEffect(() => { (async () => { try { setIsLoading(true); const resp = await fetch("https://jsonplaceholder.typicode.com/posts"); const data = await resp.json(); setPosts(data.filter((post: ISinglePost) => post.userId === 1)); setIsLoading(false); } catch (err) { setError(err); setIsLoading(false); } })(); }, []); return { isLoading, posts, error }; }
- Извлечена логика, которая присутствовала в компоненте
PostContainer
, в хук. - Этот хук вернет объект, содержащий значения
isLoading
,posts
иerror
.
PostContainer
. Затем, вместо того, чтобы передавать данные контейнера презентационным компонентам в качестве пропса, мы можем напрямую использовать этот хук внутри презентационного компонента Posts
.
Внесем следующие изменения в компонент Posts
.
Используя хуки, мы устранили дополнительный слой компонента, который присутствовал поверх этих презентационных компонентов. С хуками мы достигли тех же результатов, что и с шаблоном контейнерные/презентационные компоненты.// components/Posts.tsx import { ISinglePost } from "../Definitions"; import usePosts from "../hooks/usePosts"; import SinglePost from "./SinglePost"; export default function Posts(props: { posts: ISinglePost[] }) { const { isLoading, posts, error } = usePosts(); return ( <ul style={{ display: "flex", flexDirection: "column", alignItems: "center" }} > {isLoading ? ( <span>Loading...</span> ) : posts ? ( posts.map((post: ISinglePost) => <SinglePost {...post} />) ) : ( <span>{JSON.stringify(error)}</span> )} </ul> ); }
Итоги
Итак, в этой статье мы рассмотрели:- Разделение ответственности.
- Контейнерные и презентационные компоненты
- Зачем нам нужны эти компоненты
- Как хуки могут заменить компоненты-контейнеры
Node JS и React - как создать фулстек приложение. Полное руководство
2 года назад·3 мин. на чтение
React приложение и бэкенд на NodeJS это хорошее сочетание, которое подойдет для реализации практически любых сервисов. Эта статья поможет вам быстро создать фулстек приложение.
Необходимые инструменты
Перед началом нужно убедиться, что на компьютере установлены все необходимые библиотеки, IDE и ПО, а именно:- NodeJS и npm. Их можно скачать с официального сайта nodejs.org. npm установится автоматически вместе с NodeJS.
- Предпочитаемый IDE, например, Visual Studio Code.
- Опционально, установить git для удобной работы с кодом.
О приложении
В этой статье напишем приложение, которое будет получать и отображать список дел. Структура папок будет выглядеть следующим образом.app/ frontend/ backend/
Создание бэкэнда на NodeJS
Запустим команду в папкеapp/backend
для инициализации проекта:
Эта команда создаст файлnpm init -y
package.json
. Этот файл содержит как общую информацию о проекте (название, версия, описание и т.д.), так и информацию о зависимостях, скрипты для запуска, сборки и тестирования.
Для создания сервера будем использовать express. Установим его с помощью команды:
Создадим файлnpm i express
index.js
, который будет содержать код для запуска сервера. Этот код запускает веб сервер на порту 3010
, если он не задан в переменных среды.
// backend/index.js const express = require('express'); const PORT = process.env.PORT || 3010; const app = express(); app.use((req, res, next) => { res.setHeader('Access-Control-Allow-Origin', '*'); next(); }); app.listen(PORT, () => { console.log(`Server listening on ${PORT}`); });
package.json
. В результате сможем запускать наш сервер с помощью команды npm start
.
Из директории// backend/package.json ... "scripts": { "start": "node ./index.js" }, ...
app/backend
запустим команду npm start
. Если ошибок нет, получим сообщение, что сервер прослушивает порт 3010.
PS C:\tutorials-coding\nodejs-react-app\backend> npm start > backend@1.0.0 start > node ./index.js Server listening on 3010
Создание API
API это интерфейс, с помощью которого React приложение будет общаться с веб-сервером, т.е. запрашивать, изменять или удалять данные. В нашем случае мы создадим API для получения списка дел в формате JSON. Создадим файлtodo-items.json
c объектами todo. Этот массив будем отдавать по запросу /api/todo-items
.
[ { "id": 1, "text": "Изучить NodeJS", "done": true }, { "id": 2, "text": "Изучить ReactJS", "done": true }, { "id": 3, "text": "Написать приложение", "done": false } ]
/api/todo-items
. React приложение будет отправлять GET
запрос на этот эндпоинт.
Для того чтобы изменения вступили в силу, нужно перезапустить NodeJS сервер. Для остановки скрипта - в терминале, в котором запущен// backend/index.js // ... const todoItems = require('./todo-items.json'); app.get('/api/todo-items', (req, res) => { res.json({ data: todoItems }); }); app.listen(PORT, () => { console.log(`Server listening on ${PORT}`); });
npm start
, нужно нажать Ctrl + C
(Command + C
). Далее снова запускаем npm start
.
Для проверки эндпоинта, в браузере перейдем по адресу http://localhost:3010/api/todo-items
. В результате получим, такой ответ.

Создание фронтенда на React
В папкеapp/
откроем новый терминал и запустим команду для создания React приложения, где frontend
имя нашего приложения.
Дождемся установки всех зависимостей. В терминале перейдем в папкуnpx create-react-app@latest frontend
frontend
.
Установим библиотеку bootstrap для дальнейшего использования готовых компонентов.cd ./frontend
Заимпортируемnpm install react-bootstrap bootstrap
bootstrap.min.css
в файле frontend/src/index.js
.
Запустим приложение командойimport 'bootstrap/dist/css/bootstrap.min.css';
npm start
.
Получим следующее сообщение. Перейдем по указанному адресу в браузере.npm start
Compiled successfully! You can now view frontend in the browser. Local: http://localhost:3003 On Your Network: http://192.168.99.1:3003 Note that the development build is not optimized. To create a production build, use npm run build.
Отправка HTTP запроса из React в NodeJS
К этому моменту у нас уже есть рабочий сервер, который умеет принимать запросы и отдавать данные. Сделаем запрос на/api/todo-items
из React приложения. Для этого вызовем функцию fetch
из хука useEffect
в файле App.js
.
Открыв приложение в браузере, получим такой результат.// frontend/src/App.js import { useState, useEffect } from 'react'; import Form from 'react-bootstrap/Form'; import './App.css'; function App() { const [todoItems, setTodoItems] = useState([]); useEffect(() => { fetch('http://localhost:3010/api/todo-items') .then((res) => res.json()) .then((result) => setTodoItems(result.data)); }, []); return ( <div> {todoItems.map((item) => ( <Form.Group key={item.id} className="app__todo-item"> <Form.Check type="checkbox" checked={item.done} /> <Form.Control type="text" value={item.text} /> </Form.Group> ))} </div> ); } export default App;
