Перевод туториала официальной документации библиотеки React.js.
Мыслим в стиле React
React, на наш взгляд, это лучший способ построить большое, быстрое веб-приложение с помощью JavaScript. По нашему опыту в Facebook и Instagram, приложение на React также очень хорошо масштабируется.
Одно из многих замечательных свойств React — это принцип "Как вы проектируете приложение, также вы и создаете его". В этом туториале вы пройдете весь мыслительный процесс проектирования и создания приложения React, отображающего таблицу данных для поиска товара.
Начнем с макета
Представим, что у нас имеется некое JSON API и макет от нашего дизайнера. Макет выглядит следующим образом:
Наше JSON API возвращает некоторые данные в следующем виде:
[
{category: "Sporting Goods", price: "$49.99", stocked: true, name: "Football"},
{category: "Sporting Goods", price: "$9.99", stocked: true, name: "Baseball"},
{category: "Sporting Goods", price: "$29.99", stocked: false, name: "Basketball"},
{category: "Electronics", price: "$99.99", stocked: true, name: "iPod Touch"},
{category: "Electronics", price: "$399.99", stocked: false, name: "iPhone 5"},
{category: "Electronics", price: "$199.99", stocked: true, name: "Nexus 7"}
];
Шаг 1: Переводим пользовательский интерфейс в иерархию Компонентов
Первое, что мы делаем, это рисуем на макете боксы вокруг каждого компонента (и подчиненных компонентов) и присваиваем им названия. Если вы работаете с дизайнером, он, возможно, уже сделал это, так что необходимо поговорить с ним! Может оказаться, что названия слоев в Photoshop вполне подойдут для наименования ваших React компонентов!
Но откуда узнать, каким должен быть конечный отдельный компонент? Просто используйте те же методы для определения, что и при создании новой функции или объекта. Одним из таких методов является Принцип единственной ответственности, то есть компонент, в идеале, должен создавать только одну вещь/сущность. Если компонент создает несколько повторяемых в других компонентах сущностей, то он должен быть разложен на более мелкие компоненты.
Чем чаще вы будете отображать модель данных JSON в виде пользовательского интерфейса, тем быстрее вы придете к тому, что если модель данных построена корректно, то и ваш пользовательский интерфейс (и, следовательно, структура компонентов) выглядит красиво. Причина в том, что пользовательский интерфейс и модель данных, как правило, придерживаются одной и той же информационной архитектуры, а это означает, что разделение пользовательского интерфейса на составляющие часто является тривиальной задачей — "Просто разбейте его на компоненты, которые представляют ровно один кусочек вашей модели данных".
Как видно из макета — у нас есть пять компонентов в нашем простом приложении. Мы выделили разноцветными боксами каждый из компонентов.
FilterableProductTable
(оранжевый): содержит всю нашу таблицуSearchBar
(синий): принимает весь пользовательский вводProductTable
(зеленый): отображает и фильтрует набор данных основанных на пользовательском вводеProductCategoryRow
(бирюзовый): отображает заголовок для каждой категорииProductRow
(красный): отображает строку для каждого товара
Если вы посмотрите на ProductTable
, вы увидите, что заголовок таблицы (содержащий ярлыки "Name" и "Price") не выделен в отдельный компонент. Это вопрос предпочтений, и есть аргументы для того или иного варианта. Для этого примера, мы оставили заголовок в составе ProductTable
, потому что он является частью визуализации набора данных, которая относится к ответственности ProductTable
. Однако, если заголовок был бы более сложным (например: мы должны были бы добавить возможность для сортировки по столбцам), конечно, мы бы выделили бы его в отдельный компонент ProductTableHeader
.
Теперь, когда мы определили компоненты в нашем макете, давайте оформим их в виде иерархии. Это легко. Компоненты, которые входят в другой компонент в макете, должны выглядеть как потомки в иерархии:
FilterableProductTable
SearchBar
ProductTable
ProductCategoryRow
ProductRow
Шаг 2: Создаем статическую версию в React
<div id="container">
<!-- Содержание этого элемента будет заменено вашим компонентом. -->
</div>
body {
padding: 5px
}
class ProductCategoryRow extends React.Component {
render() {
return <tr><th colSpan="2">{this.props.category}</th></tr>;
}
}
class ProductRow extends React.Component {
render() {
var name = this.props.product.stocked ?
this.props.product.name :
<span style={{color: 'red'}}>
{this.props.product.name}
</span>;
return (
<tr>
<td>{name}</td>
<td>{this.props.product.price}</td>
</tr>
);
}
}
class ProductTable extends React.Component {
render() {
var rows = [];
var lastCategory = null;
this.props.products.forEach(function(product) {
if (product.category !== lastCategory) {
rows.push(<ProductCategoryRow category={product.category} key={product.category} />);
}
rows.push(<ProductRow product={product} key={product.name} />);
lastCategory = product.category;
});
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</table>
);
}
}
class SearchBar extends React.Component {
render() {
return (
<form>
<input type="text" placeholder="Search..." />
<p>
<input type="checkbox" />
{' '}
Only show products in stock
</p>
</form>
);
}
}
class FilterableProductTable extends React.Component {
render() {
return (
<div>
<SearchBar />
<ProductTable products={this.props.products} />
</div>
);
}
}
var PRODUCTS = [
{category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'},
{category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'},
{category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'},
{category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'},
{category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'},
{category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'}
];
ReactDOM.render(
<FilterableProductTable products={PRODUCTS} />,
document.getElementById('container')
);
Теперь у вас есть ваша иерархия Компонентов, пришло время реализовать свое приложение. Самый простой способ — начать со статической версии, в которой отображается модель данных и пользовательский интерфейс, но нет интерактивности. Разделение статической и интерактивной частей — хорошое решение, т.к. задача по реализации статической версии — это ввод большого количества текста с клавиатуры с наименьшим мыслительным процессом, тогда как реализация интерактивности наоборот требует большого мыслительного процесса и малого ввода с клавиатуры. Далее мы увидим — почему так.
Реализация статической версии — это отображение (рендеринг) модели данных, в процессе которого вы создаете Компоненты, которые используют другие Компоненты и передают данные посредством props (Свойства). props являются инструментом передачи данных от предка к потомку в иерархии Компонентов React. В React есть такое понятие, как state (Состояние) — никогда не используйте Состояние (state) при создании статической версии. Основное предназначение Состояния — интерактивность, оно необходимо для передачи и фиксирования данных, которые меняются с течением времени. Поскольку, в данный момент, вы создаете статическую версию приложения — вы не нуждаетесь в использовании Состояния.
Вы можете реализовывать приложение сверху-вниз или снизу-вверх. То есть, вы можете начать с построения Компонентов более высокого уровня иерархии (начиная с FilterableProductTable
) или наоборот с низкого уровня (ProductRow
). В более простых приложениях, как правило, легче идти сверху вниз, а в более крупных — снизу вверх и параллельно писать тесты по мере реализации Компонентов.
В конце этого шага, у вас будет библиотека повторно используемых Компонентов, которые отображают вашу модель данных. Компоненты будут иметь только метод render()
, так как это статическая версия. Компонент в верхней части иерархии (FilterableProductTable
) получит модель данных посредством props. Если вы внесете изменение в базовую модель данных и вызовете ReactDOM.render()
заново — пользовательский интерфейс будет обновлен. Не правда ли — в этом нет ничего сложного, т.к это действительно очень просто? Односторонний поток данных React (также называемый односторонним связыванием) обеспечивает модульность и скорость.
Небольшое отступление: Свойства (props) и Состояние (state)
В React существуют две "модели" данных: props и state. Важно понимать различие между ними. Если вы не уверены, что знаете разницу — перечитайте соответствующий раздел официальной документации.
Шаг 3: Определяем минимальное (но достаточное) представление Cостояния (state) пользовательского интерфейса
Для того, чтобы сделать пользовательский интерфейс интерактивным, вам необходимо иметь возможность регистрировать изменения в базовой модели данных. В React это делается просто с помощью state.
Для построения корректного приложения, в первую очередь, вам необходимо обдумать минимальный набор изменяемых состояний необходимых для вашего приложения. Придерживайтесь принципа DRY: Don't Repeat Yourself (Не повторяйся). В идеале — минимальное представление Состояния вашего приложения не должно содержать чего либо, что может быть вычислено на основании имеющихся данных (в props и state) в момент времени, когда это необходимо. Например: Если вы строите приложение, отображающее список дел, оставайтесь в пределах массива, содержащего записи дел — не создавайте отдельной переменной Состояния, отображающей количество дел. Вместо этого, в тот момент когда вам необходимо отобразить количество дел — просто возьмите длину имеющегося массива.
Продумаем все единицы данных в нашем приложении. Мы имеем:
- Оригинальный список продуктов
- Поисковый текст, введенный пользователем
- Значение чекбокса
- Отфильтрованный список продуктов
Давайте пройдемся по каждому пункту и определим — является ли он Состоянием. Для каждой единицы данных нам надо задать три вопроса:
- Передаются ли эти данные посредством props от предка? Если да, то это скорее всего не Состояние.
- Остаются ли эти данные неизменными с течением времени? Если да, то это скорее всего не Состояние.
- Можете ли вы вычислить эти данные на основании имеющихся (в props и state) в вашем Компоненте? Если да, то это не Состояние.
Оригинальный список продуктов передается посредством props — значит это не Состояние. Поисковый текст и значение чекбокса могут изменяться с течением времени и не могут быть вычислены из имеющихся данных — это Состояния. И последнее: Отфильтрованный список продуктов может быть вычислен комбинированием оригинального списка продуктов, поисковым текстом и значением чекбокса — это не Состояние.
В итоге, наши Состояния:
- Поисковый текст, введенный пользователем
- Значение чекбокса
Шаг 4: Определяем где наше Состояние будет размещаться
<div id="container">
<!-- Содержание этого элемента будет заменено вашим компонентом. -->
</div>
body {
padding: 5px
}
class ProductCategoryRow extends React.Component {
render() {
return (<tr><th colSpan="2">{this.props.category}</th></tr>);
}
}
class ProductRow extends React.Component {
render() {
var name = this.props.product.stocked ?
this.props.product.name :
<span style={{color: 'red'}}>
{this.props.product.name}
</span>;
return (
<tr>
<td>{name}</td>
<td>{this.props.product.price}</td>
</tr>
);
}
}
class ProductTable extends React.Component {
render() {
var rows = [];
var lastCategory = null;
this.props.products.forEach((product) => {
if (product.name.indexOf(this.props.filterText) === -1 || (!product.stocked && this.props.inStockOnly)) {
return;
}
if (product.category !== lastCategory) {
rows.push(<ProductCategoryRow category={product.category} key={product.category} />);
}
rows.push(<ProductRow product={product} key={product.name} />);
lastCategory = product.category;
});
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</table>
);
}
}
class SearchBar extends React.Component {
render() {
return (
<form>
<input type="text" placeholder="Search..." value={this.props.filterText} />
<p>
<input type="checkbox" checked={this.props.inStockOnly} />
{' '}
Only show products in stock
</p>
</form>
);
}
}
class FilterableProductTable extends React.Component {
constructor(props) {
super(props);
this.state = {
filterText: '',
inStockOnly: false
};
}
render() {
return (
<div>
<SearchBar
filterText={this.state.filterText}
inStockOnly={this.state.inStockOnly}
/>
<ProductTable
products={this.props.products}
filterText={this.state.filterText}
inStockOnly={this.state.inStockOnly}
/>
</div>
);
}
}
var PRODUCTS = [
{category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'},
{category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'},
{category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'},
{category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'},
{category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'},
{category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'}
];
ReactDOM.render(
<FilterableProductTable products={PRODUCTS} />,
document.getElementById('container')
);
Итак, мы определили минимальный набор Состояний приложения. Следующим шагом нам необходимо определить какой компонент изменяет или владеет этим Состоянием.
Запомните: В React работает односторонняя передача данных вниз по иерархии Компонентов. Возможно, из этого не сразу понятно какой компонент должен быть владельцем Состояния. Эта часть часто является достаточно сложной для новичков — поэтому следуйте следующим шагам, для выяснения этого вопроса:
Для каждой единицы Состояния вашего приложения:
- Определите все Компоненты, которые отображают (производят рендеринг) что-либо на основании этого Состояния.
- Найдите общего предка для этих Компонентов (единый Компонент по иерархии выше для всех Компонентов, которым необходимо это Состояние).
- Найденного общего предка или любой Компонент в иерархии вышего него можно назначить владельцем Состояния.
- Если общий предок отсутствует в вашей иерархии Компонентов — вам необходимо создать Компонент более высокого уровня просто для назначения его владельцем Состояния.
Итак, давайте применим эту стратегию к нашему приложению:
- Компонент
ProductTable
нуждается в Состоянии для фильтрации списка продуктов, в то же время КомпонентSearchBar
нуждается в Состоянии для отображения поискового запроса и состояния чекбокса. - Общим предком для них является Компонент
FilterableProductTable
. - Таким образом, концептуально, точка соприкосновения фильтрации списка и выбранных значений находится в Компоненте
FilterableProductTable
Отлично, мы определили, что наше Состояние должно быть размещено в Компоненте FilterableProductTable
. В первую очердь добавим свойство экземпляра this.state = {filterText: '', inStockOnly: false}
в constructor
Компонента FilterableProductTable
для определения начального Состояния нашего приложения. Затем передадим filterText
и inStockOnly
в Компоненты ProductTable
и SearchBar
посредством props. И конечным шагом — используем props для фильтрации строк в ProductTable
и установки значений полей формы в SearchBar
.
Вы можете запустить приложение и посмотреть как оно будет вести себя: установите значение filterText
в конструкторе Компонента FilterableProductTable
равным 'ball'
и перезагрузите приложение. Вы увидите, что таблица данных корректно обновлена.
Шаг 5: Добавляем обратный поток данных
<div id="container">
<!-- Содержание этого элемента будет заменено вашим компонентом. -->
</div>
body {
padding: 5px
}
class ProductCategoryRow extends React.Component {
render() {
return (<tr><th colSpan="2">{this.props.category}</th></tr>);
}
}
class ProductRow extends React.Component {
render() {
var name = this.props.product.stocked ?
this.props.product.name :
<span style={{color: 'red'}}>
{this.props.product.name}
</span>;
return (
<tr>
<td>{name}</td>
<td>{this.props.product.price}</td>
</tr>
);
}
}
class ProductTable extends React.Component {
render() {
var rows = [];
var lastCategory = null;
this.props.products.forEach((product) => {
if (product.name.indexOf(this.props.filterText) === -1 || (!product.stocked && this.props.inStockOnly)) {
return;
}
if (product.category !== lastCategory) {
rows.push(<ProductCategoryRow category={product.category} key={product.category} />);
}
rows.push(<ProductRow product={product} key={product.name} />);
lastCategory = product.category;
});
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</table>
);
}
}
class SearchBar extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
handleChange() {
this.props.onUserInput(
this.filterTextInput.value,
this.inStockOnlyInput.checked
);
}
render() {
return (
<form>
<input
type="text"
placeholder="Search..."
value={this.props.filterText}
ref={(input) => this.filterTextInput = input}
onChange={this.handleChange}
/>
<p>
<input
type="checkbox"
checked={this.props.inStockOnly}
ref={(input) => this.inStockOnlyInput = input}
onChange={this.handleChange}
/>
{' '}
Only show products in stock
</p>
</form>
);
}
}
class FilterableProductTable extends React.Component {
constructor(props) {
super(props);
this.state = {
filterText: '',
inStockOnly: false
};
this.handleUserInput = this.handleUserInput.bind(this);
}
handleUserInput(filterText, inStockOnly) {
this.setState({
filterText: filterText,
inStockOnly: inStockOnly
});
}
render() {
return (
<div>
<SearchBar
filterText={this.state.filterText}
inStockOnly={this.state.inStockOnly}
onUserInput={this.handleUserInput}
/>
<ProductTable
products={this.props.products}
filterText={this.state.filterText}
inStockOnly={this.state.inStockOnly}
/>
</div>
);
}
}
var PRODUCTS = [
{category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'},
{category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'},
{category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'},
{category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'},
{category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'},
{category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'}
];
ReactDOM.render(
<FilterableProductTable products={PRODUCTS} />,
document.getElementById('container')
);
К текущему шагу мы создали приложение, которое корректно передает и использует props и state двигаясь по иерархии Компонентов вниз. Настало время добавит поддержку передачи данных в обратном направлении: компонентам формы, которые расположены ниже по иерархии, необходимо каким-то образом обновлять Состояние в Компоненте FilterableProductTable
.
Механизм React для создания этого потока данных отлично проработан, чтобы создать легкое понимание того, как работает программа, но он требует немного больше ввода с клавиатуры, чем традиционное двустороннее связывание данных.
Если вы попробуете ввести текст или отметить чекбокс в текущей версии примера, вы обнаружите, что React игнорирует ваш ввод. Это умышленно установлено нами, т.к. мы установили значение свойства value
в input
всегда эквивалентным state
передаваемому из Компонента FilterableProductTable
.
Давайте подумаем — что мы хотим чтобы произошло. Мы хотим, чтобы каждый раз, когда пользователь вносит изменения в форму, обновлялось Состояние для отображения пользовательского ввода. Поскольку Компоненты должны обновлять только собственное Состояние, Компоненту FilterableProductTable
необходимо передать в Компонент SearchBar
механизм обратного вызова, который будет сигнализировать каждый раз, когда Состояние должно быть обновлено. Мы можем использовать событие onChange
в компонентах формы для сообщения об этом. Тогда, обратный вызов переданный Компонентом FilterableProductTable
вызовет setState()
и приложение будет обновлено.
Хотя это и выглядит сложным, но на самом деле это всего лишь несколько строк кода. И реально прозрачно видно как ваши данные передаются через все приложение.
На этом все
Надеюсь, этот туториал дал вам представление о том, как следует мыслить при построении компонентов и приложений React. Хотя нам пришлось набрать немного больше кода, чем вы привыкли, помните, что код читается в разы чаще, чем пишется, а наш код читается легко за счет прозрачности и модульности. Как только вы начнете создавать большие библиотеки или приложения — вы по настоящему оцените эту прозрачность и модульность, а возможность повторного использования неизменно приведет к тому, что вы будете набирать все меньше и меньше кода.
Первоисточник: React — Quick Start — Thinking in React
Комментарии (16)
Ovoshlook
09.01.2017 08:58+1Я так понимаю, что вы данной статьей хотели показать, что вы не ленивый
Ибо только тут ленивый не писал tutorial про React…
Zmeu213
09.01.2017 10:55+1Смысл react'a хранить текущий state клиента, но разве это хорошо? Принципы stateless никто не отменял.
Maxpain154
09.01.2017 12:56Вас никто не обязывает использовать состояние. Можете писать stateless компоненты, реализуя их чистыми функциями.
oklas
10.01.2017 20:25Речь идёт в частности о состоянии самого компонента. Например выпадающий список (DropDown). Как минимум имеет собственное состояние открыто (open). Когда на него кликнули он открывается и показывает содержимое (open:true), пока что-либо не выберут или пока оно не закроется по другому поводу (open:false).
Zmeu213
10.01.2017 20:32Да, и, насколько я понимаю react, состояние этих элементов хранит само приложение а не браузер пользователя. Насколько хорошая эта практика?
alexzzam
09.01.2017 14:02+2А расскажите, пожалуйста, что делает вот эта строчка?
ref={(input) => this.filterTextInput = input}
AsTex
09.01.2017 14:11ref — ссылка на ваш элемент.
В данном случае, при рендере объекта переменной this.filterTextInput будет присвоен этот ReactElement
oklas
10.01.2017 20:40Этот ref используется для получения ссылки на наш элемент. React вызывает функцию обработчик прописанную в
ref
передеавая ей DOM элемент когда компонент монтируется иnull
когда компонент демонтируется. Документация рекомендует избегать использования этой возможности. Собственно что конкретно делается уже ответили (в полеfilterTextInput
сохранили ссылку для последующего использования).
AsTex
При использовании ES2015 стоит использовать let вместо var.
Да и при построении React компонентов, лучше описывать типы параметров, передающихся в компонент, ведь читаем мы код чаще, чем пишем ;)
vtikunov
Это всего лишь перевод и исходники оттуда же.
vtikunov
Мне, кажется, основный посыл в архитектуре приложения React и наличие/отсуствие let/var и валидаторов PropTypes не наносит смыслового ущерба туториалу из раздела «Быстрый старт».
Hydro
А мне кажется, что оригинальная статья, да и вообще вся документация по React написана в такой легкой манере, что даже мне, не особо сведущему человеку в английском, легко удавалось прочитать её в оригинале без гугл транслейта.