В этой части перевода учебного курса по React мы поговорим об архитектуре React-приложений. В частности, обсудим популярный паттерн Container/Component.
→ Часть 1: обзор курса, причины популярности React, ReactDOM и JSX
→ Часть 2: функциональные компоненты
→ Часть 3: файлы компонентов, структура проектов
→ Часть 4: родительские и дочерние компоненты
→ Часть 5: начало работы над TODO-приложением, основы стилизации
→ Часть 6: о некоторых особенностях курса, JSX и JavaScript
→ Часть 7: встроенные стили
→ Часть 8: продолжение работы над TODO-приложением, знакомство со свойствами компонентов
→ Часть 9: свойства компонентов
→ Часть 10: практикум по работе со свойствами компонентов и стилизации
→ Часть 11: динамическое формирование разметки и метод массивов map
→ Часть 12: практикум, третий этап работы над TODO-приложением
→ Часть 13: компоненты, основанные на классах
→ Часть 14: практикум по компонентам, основанным на классах, состояние компонентов
→ Часть 15: практикумы по работе с состоянием компонентов
→ Часть 16: четвёртый этап работы над TODO-приложением, обработка событий
→ Часть 17: пятый этап работы над TODO-приложением, модификация состояния компонентов
→ Часть 18: шестой этап работы над TODO-приложением
→ Часть 19: методы жизненного цикла компонентов
→ Часть 20: первое занятие по условному рендерингу
→ Часть 21: второе занятие и практикум по условному рендерингу
→ Часть 22: седьмой этап работы над TODO-приложением, загрузка данных из внешних источников
→ Часть 23: первое занятие по работе с формами
→ Часть 24: второе занятие по работе с формами
→ Часть 25: практикум по работе с формами
→ Часть 26: архитектура приложений, паттерн Container/Component
Занятие 44. Архитектура приложений, паттерн Container/Component
→ Оригинал
Иногда объём работы, за выполнение которой отвечает отдельный компонент, оказывается слишком большим, компоненту приходится решать слишком много задач. Использование паттерна Container/Component позволяет отделить логику функционирования приложения от логики формирования его визуального представления. Это позволяет улучшить структуру приложения, разделить ответственность за выполнение различных задач между разными компонентами.
На предыдущем практическом занятии мы создали огромный компонент, длина кода которого приближается к 150 строкам. Вот код, который у нас тогда получился:
import React, {Component} from "react"
class App extends Component {
constructor() {
super()
this.state = {
firstName: "",
lastName: "",
age: "",
gender: "",
destination: "",
isVegan: false,
isKosher: false,
isLactoseFree: false
}
this.handleChange = this.handleChange.bind(this)
}
handleChange(event) {
const {name, value, type, checked} = event.target
type === "checkbox" ?
this.setState({
[name]: checked
})
:
this.setState({
[name]: value
})
}
render() {
return (
<main>
<form>
<input
name="firstName"
value={this.state.firstName}
onChange={this.handleChange}
placeholder="First Name"
/>
<br />
<input
name="lastName"
value={this.state.lastName}
onChange={this.handleChange}
placeholder="Last Name"
/>
<br />
<input
name="age"
value={this.state.age}
onChange={this.handleChange}
placeholder="Age"
/>
<br />
<label>
<input
type="radio"
name="gender"
value="male"
checked={this.state.gender === "male"}
onChange={this.handleChange}
/> Male
</label>
<br />
<label>
<input
type="radio"
name="gender"
value="female"
checked={this.state.gender === "female"}
onChange={this.handleChange}
/> Female
</label>
<br />
<select
value={this.state.destination}
name="destination"
onChange={this.handleChange}
>
<option value="">-- Please Choose a destination --</option>
<option value="germany">Germany</option>
<option value="norway">Norway</option>
<option value="north pole">North Pole</option>
<option value="south pole">South Pole</option>
</select>
<br />
<label>
<input
type="checkbox"
name="isVegan"
onChange={this.handleChange}
checked={this.state.isVegan}
/> Vegan?
</label>
<br />
<label>
<input
type="checkbox"
name="isKosher"
onChange={this.handleChange}
checked={this.state.isKosher}
/> Kosher?
</label>
<br />
<label>
<input
type="checkbox"
name="isLactoseFree"
onChange={this.handleChange}
checked={this.state.isLactoseFree}
/> Lactose Free?
</label>
<br />
<button>Submit</button>
</form>
<hr />
<h2><font color="#000">Entered information:</font></h2>
<p>Your name: {this.state.firstName} {this.state.lastName}</p>
<p>Your age: {this.state.age}</p>
<p>Your gender: {this.state.gender}</p>
<p>Your destination: {this.state.destination}</p>
<p>Your dietary restrictions:</p>
<p>Vegan: {this.state.isVegan ? "Yes" : "No"}</p>
<p>Kosher: {this.state.isKosher ? "Yes" : "No"}</p>
<p>Lactose Free: {this.state.isLactoseFree ? "Yes" : "No"}</p>
</main>
)
}
}
export default App
Первый недостаток этого кода, который сразу же бросается в глаза, заключается в том, что при работе с ним его постоянно приходится прокручивать в окне редактора.
Можно заметить, что основной объём этого кода составляет логика формирования интерфейса приложения, содержимое метода render()
. Кроме того, некоторый объём кода отвечает за инициализацию состояния компонента. В компоненте есть и то, что называется «business logic» (то есть то, что реализует логику функционирования приложения). Это — код метода handleChange()
.
По результатам некоторых исследований известно, что возможность программиста воспринимать код, на который он смотрит, сильно ухудшается в том случае, если код это достаточно длинный, и программисту приходится, чтобы просмотреть его целиком, пользоваться прокруткой. Я заметил это и в ходе проведения занятий. Когда код, о котором я рассказываю, оказывается достаточно длинным, и мне постоянно приходится его прокручивать, учащимся становится сложнее его воспринимать.
Хорошо было бы, если бы мы переработали наш код, разделив между разными компонентами ответственность за формирование интерфейса приложения (то, что сейчас описано в методе render()
) и за реализацию логики функционирования приложения, то есть, по определению того, как должен выглядеть его интерфейс (соответствующий код сейчас представлен конструктором компонента, в котором инициализируется состояние, и обработчиком событий элементов управления handleChange()
). При использовании подобного подхода к проектированию приложений мы, фактически, работаем с двумя видами компонентов, при этом надо отметить, что можно столкнуться с разными названиями подобных компонентов.
Мы будем пользоваться здесь паттерном Container/Component. При его использовании приложения строят, разделяя компоненты на два вида — на компоненты-контейнеры (к ним относится слово Container в названии паттерна), и на презентационные компоненты (это — Component в названии паттерна). Иногда компоненты-контейнеры называют «умными» (smart) компонентами, или просто «контейнерами», а презентационные — «глупыми» (dumb) компонентами, или просто «компонентами». Есть и другие наименования этих видов компонентов, и, надо отметить, смысл, который вкладывается в эти наименования, может, от случая к случаю, отличаться определёнными особенностями. В целом же, общая идея рассматриваемого подхода заключается в том, что у нас есть компонент-контейнер, ответственный за хранение состояния и содержащий методы для управления состоянием, а логика формирования интерфейса передаётся другому — презентационному компоненту. Этот компонент отвечает лишь за получение от компонента-контейнера свойств и за правильное формирование интерфейса.
→ Вот материал Дэна Абрамова, в котором он исследует эту идею.
Преобразуем код нашего приложения в соответствии с паттерном Container/Component.
Для начала обратим внимание на то, что сейчас всё в приложении собрано в единственном компоненте App
. Это приложение устроено так ради максимального упрощения его структуры, но в реальных проектах компоненту App
вряд ли имеет смысл передавать задачу рендеринга формы и включать в него код, предназначенный для организации работы внутренних механизмов этой формы.
Добавим, в ту же папку, в которой находится файл App.js
, файл Form.js
, в котором будет находиться код нового компонента. Перенесём в этот файл весь код из компонента App
, а компонент App
, представленный сейчас компонентом, который основан на классе, преобразуем в функциональный компонент, основной задачей которого будет вывод компонента Form
. Не забудем импортировать компонент Form
в компонент App
. В результате код компонента App
будет выглядеть так:
import React, {Component} from "react"
import Form from "./Form"
function App() {
return (
<Form />
)
}
export default App
Вот как выглядит на данном этапе работы то, что приложение выводит на экран.
Приложение в браузере
На предыдущих занятиях я говорил вам о том, что предпочитаю, чтобы компонент App
представлял собой нечто вроде «оглавления» приложения, в котором указано, в каком порядке на страницу выводятся её разделы, представленные другими компонентами, которым делегированы задачи по рендерингу крупных фрагментов приложения.
Мы немного улучшили структуру приложения, но основную проблему, выражающуюся в том, что на один компонент возлагается слишком большая ответственность, пока не решили. Мы просто перенесли всё, что раньше было в компоненте App
, в компонент Form
. Поэтому теперь займёмся решением этой проблемы. Для этого создадим, в той же папке, в которой находятся файлы Form.js
и App.js
, ещё один файл — FormComponent.js
. Этот файл будет представлять презентационный компонент, ответственный за визуализацию формы. На самом деле, назвать его можно и по-другому, можно и иначе структурировать файлы компонентов, всё зависит от нужд и масштабов конкретного проекта. Файл Form.js
будет содержать логику функционирования формы, то есть — код компонента-контейнера. Поэтому переименуем его в FormContainer.js
и поменяем команду импорта в коде компонента App
, приведя её к такому виду:
import Form from "./FormContainer"
Можно ещё и переименовать компонент Form
в FormContainer
, но мы этого делать не будем. Теперь перенесём код, ответственный за рендеринг формы, из файла FormContainer.js
в файл FormComponent.js
.
Компонент FormComponent
будет функциональным. Вот как будет выглядеть его код на данном этапе работы:
function FormComponent(props) {
return (
<main>
<form>
<input
name="firstName"
value={this.state.firstName}
onChange={this.handleChange}
placeholder="First Name"
/>
<br />
<input
name="lastName"
value={this.state.lastName}
onChange={this.handleChange}
placeholder="Last Name"
/>
<br />
<input
name="age"
value={this.state.age}
onChange={this.handleChange}
placeholder="Age"
/>
<br />
<label>
<input
type="radio"
name="gender"
value="male"
checked={this.state.gender === "male"}
onChange={this.handleChange}
/> Male
</label>
<br />
<label>
<input
type="radio"
name="gender"
value="female"
checked={this.state.gender === "female"}
onChange={this.handleChange}
/> Female
</label>
<br />
<select
value={this.state.destination}
name="destination"
onChange={this.handleChange}
>
<option value="">-- Please Choose a destination --</option>
<option value="germany">Germany</option>
<option value="norway">Norway</option>
<option value="north pole">North Pole</option>
<option value="south pole">South Pole</option>
</select>
<br />
<label>
<input
type="checkbox"
name="isVegan"
onChange={this.handleChange}
checked={this.state.isVegan}
/> Vegan?
</label>
<br />
<label>
<input
type="checkbox"
name="isKosher"
onChange={this.handleChange}
checked={this.state.isKosher}
/> Kosher?
</label>
<br />
<label>
<input
type="checkbox"
name="isLactoseFree"
onChange={this.handleChange}
checked={this.state.isLactoseFree}
/> Lactose Free?
</label>
<br />
<button>Submit</button>
</form>
<hr />
<h2><font color="#000">Entered information:</font></h2>
<p>Your name: {this.state.firstName} {this.state.lastName}</p>
<p>Your age: {this.state.age}</p>
<p>Your gender: {this.state.gender}</p>
<p>Your destination: {this.state.destination}</p>
<p>Your dietary restrictions:</p>
<p>Vegan: {this.state.isVegan ? "Yes" : "No"}</p>
<p>Kosher: {this.state.isKosher ? "Yes" : "No"}</p>
<p>Lactose Free: {this.state.isLactoseFree ? "Yes" : "No"}</p>
</main>
)
}
Если взглянуть на этот код, то становится понятным, что простым его переносом из файла в файл мы ограничиться не можем, так как сейчас здесь присутствуют ссылки на состояние (например — this.state.firstName
) и на обработчик событий (this.handleChange
), которые раньше находились в том же компоненте, основанном на классе, в котором находился и этот код рендеринга. Теперь же всё то, что раньше бралось из того же класса, в котором находился код рендеринга, будет браться из свойств, передаваемых компоненту. Тут есть и некоторые другие проблемы. Сейчас мы исправим этот код, но сначала вернёмся к коду компонента Form
, который сейчас находится в файле FormContainer.js
.
Его метод render()
теперь пуст. Нам нужно, чтобы в этом методе выводился бы компонент FormComponent
и нужно организовать передачу ему необходимых свойств. Импортируем FormComponent
в файл Form
и выведем FormComponent
в методе render()
, передав ему обработчик событий, и, в виде объекта, состояние. Теперь код компонента Form
будет выглядеть так:
import React, {Component} from "react"
import FormComponent from "./FormComponent"
class Form extends Component {
constructor() {
super()
this.state = {
firstName: "",
lastName: "",
age: "",
gender: "",
destination: "",
isVegan: false,
isKosher: false,
isLactoseFree: false
}
this.handleChange = this.handleChange.bind(this)
}
handleChange(event) {
const {name, value, type, checked} = event.target
type === "checkbox" ?
this.setState({
[name]: checked
})
:
this.setState({
[name]: value
})
}
render() {
return(
<FormComponent
handleChange={this.handleChange}
data={this.state}
/>
)
}
}
export default Form
Исправим код компонента FormComponent
, приведя его к следующему виду:
import React from "react"
function FormComponent(props) {
return (
<main>
<form>
<input
name="firstName"
value={props.data.firstName}
onChange={props.handleChange}
placeholder="First Name"
/>
<br />
<input
name="lastName"
value={props.data.lastName}
onChange={props.handleChange}
placeholder="Last Name"
/>
<br />
<input
name="age"
value={props.data.age}
onChange={props.handleChange}
placeholder="Age"
/>
<br />
<label>
<input
type="radio"
name="gender"
value="male"
checked={props.data.gender === "male"}
onChange={props.handleChange}
/> Male
</label>
<br />
<label>
<input
type="radio"
name="gender"
value="female"
checked={props.data.gender === "female"}
onChange={props.handleChange}
/> Female
</label>
<br />
<select
value={props.data.destination}
name="destination"
onChange={props.handleChange}
>
<option value="">-- Please Choose a destination --</option>
<option value="germany">Germany</option>
<option value="norway">Norway</option>
<option value="north pole">North Pole</option>
<option value="south pole">South Pole</option>
</select>
<br />
<label>
<input
type="checkbox"
name="isVegan"
onChange={props.handleChange}
checked={props.data.isVegan}
/> Vegan?
</label>
<br />
<label>
<input
type="checkbox"
name="isKosher"
onChange={props.handleChange}
checked={props.data.isKosher}
/> Kosher?
</label>
<br />
<label>
<input
type="checkbox"
name="isLactoseFree"
onChange={props.handleChange}
checked={props.data.isLactoseFree}
/> Lactose Free?
</label>
<br />
<button>Submit</button>
</form>
<hr />
<h2><font color="#000">Entered information:</font></h2>
<p>Your name: {props.data.firstName} {props.data.lastName}</p>
<p>Your age: {props.data.age}</p>
<p>Your gender: {props.data.gender}</p>
<p>Your destination: {props.data.destination}</p>
<p>Your dietary restrictions:</p>
<p>Vegan: {props.data.isVegan ? "Yes" : "No"}</p>
<p>Kosher: {props.data.isKosher ? "Yes" : "No"}</p>
<p>Lactose Free: {props.data.isLactoseFree ? "Yes" : "No"}</p>
</main>
)
}
export default FormComponent
Здесь мы исправили код с учётом того, что компонент теперь получает данные и ссылку на обработчик событий через свойства.
После всех этих преобразований не изменится ни внешний вид формы, ни то, как она работает, но структуру кода проекта мы улучшили, хотя размер кода компонента FormComponent
всё ещё оказывается довольно большим. Однако теперь этот код решает лишь одну задачу, он отвечает только за визуализацию формы. Поэтому работать с ним теперь гораздо легче.
В результате мы добились разделения ответственности между компонентами. Компонент Form
из файла FormContainer.js
теперь занят исключительно логикой функционирования приложения, а компонент FormComponent
из файла FormComponent.js
содержит только код, формирующий интерфейс приложения. Компонент App
теперь отвечает лишь за сборку страницы из крупных блоков.
Тут стоит отметить, что с учётом существования библиотек наподобие Redux
и недавно вышедшего API Context
, рассмотренный здесь паттерн Container/Component уже не так актуален, как прежде. Например, средствами Redux можно поддерживать глобальное состояние приложения, которым могут пользоваться компоненты.
Итоги
На этом занятии мы рассмотрели применение паттерна Container/Component, который нацелен на разделение компонентов на те, которые отвечают за формирование интерфейса приложения, и на те, которые отвечают за хранение состояния и за логику функционирования приложения. Применение этого паттерна помогает улучшить структуру кода React-приложений и облегчает процесс разработки. В следующий раз мы займёмся работой над курсовым проектом.
Уважаемые читатели! Какие паттерны проектирования вы используете при разработке React-приложений?
Автор: ru_vds