[Из песочницы] Мыслим в стиле React
Перевод туториала официальной документации библиотеки 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
body {
padding: 5px
}
class ProductCategoryRow extends React.Component {
render() {
return {this.props.category} ;
}
}
class ProductRow extends React.Component {
render() {
var name = this.props.product.stocked ?
this.props.product.name :
{this.props.product.name}
;
return (
{name}
{this.props.product.price}
);
}
}
class ProductTable extends React.Component {
render() {
var rows = [];
var lastCategory = null;
this.props.products.forEach(function(product) {
if (product.category !== lastCategory) {
rows.push( );
}
rows.push( );
lastCategory = product.category;
});
return (
Name
Price
{rows}
);
}
}
class SearchBar extends React.Component {
render() {
return (
);
}
}
class FilterableProductTable extends React.Component {
render() {
return (
);
}
}
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(
,
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: Определяем где наше Состояние будет размещаться
body {
padding: 5px
}
class ProductCategoryRow extends React.Component {
render() {
return ({this.props.category} );
}
}
class ProductRow extends React.Component {
render() {
var name = this.props.product.stocked ?
this.props.product.name :
{this.props.product.name}
;
return (
{name}
{this.props.product.price}
);
}
}
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( );
}
rows.push( );
lastCategory = product.category;
});
return (
Name
Price
{rows}
);
}
}
class SearchBar extends React.Component {
render() {
return (
);
}
}
class FilterableProductTable extends React.Component {
constructor(props) {
super(props);
this.state = {
filterText: '',
inStockOnly: false
};
}
render() {
return (
);
}
}
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(
,
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: Добавляем обратный поток данных
body {
padding: 5px
}
class ProductCategoryRow extends React.Component {
render() {
return ({this.props.category} );
}
}
class ProductRow extends React.Component {
render() {
var name = this.props.product.stocked ?
this.props.product.name :
{this.props.product.name}
;
return (
{name}
{this.props.product.price}
);
}
}
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( );
}
rows.push( );
lastCategory = product.category;
});
return (
Name
Price
{rows}
);
}
}
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 (
);
}
}
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 (
);
}
}
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(
,
document.getElementById('container')
);
К текущему шагу мы создали приложение, которое корректно передает и использует props и state двигаясь по иерархии Компонентов вниз. Настало время добавит поддержку передачи данных в обратном направлении: компонентам формы, которые расположены ниже по иерархии, необходимо каким-то образом обновлять Состояние в Компоненте FilterableProductTable
.
Механизм React для создания этого потока данных отлично проработан, чтобы создать легкое понимание того, как работает программа, но он требует немного больше ввода с клавиатуры, чем традиционное двустороннее связывание данных.
Если вы попробуете ввести текст или отметить чекбокс в текущей версии примера, вы обнаружите, что React игнорирует ваш ввод. Это умышленно установлено нами, т.к. мы установили значение свойства value
в input
всегда эквивалентным state
передаваемому из Компонента FilterableProductTable
.
Давайте подумаем — что мы хотим чтобы произошло. Мы хотим, чтобы каждый раз, когда пользователь вносит изменения в форму, обновлялось Состояние для отображения пользовательского ввода. Поскольку Компоненты должны обновлять только собственное Состояние, Компоненту FilterableProductTable
необходимо передать в Компонент SearchBar
механизм обратного вызова, который будет сигнализировать каждый раз, когда Состояние должно быть обновлено. Мы можем использовать событие onChange
в компонентах формы для сообщения об этом. Тогда, обратный вызов переданный Компонентом FilterableProductTable
вызовет setState()
и приложение будет обновлено.
Хотя это и выглядит сложным, но на самом деле это всего лишь несколько строк кода. И реально прозрачно видно как ваши данные передаются через все приложение.
На этом все
Надеюсь, этот туториал дал вам представление о том, как следует мыслить при построении компонентов и приложений React. Хотя нам пришлось набрать немного больше кода, чем вы привыкли, помните, что код читается в разы чаще, чем пишется, а наш код читается легко за счет прозрачности и модульности. Как только вы начнете создавать большие библиотеки или приложения — вы по настоящему оцените эту прозрачность и модульность, а возможность повторного использования неизменно приведет к тому, что вы будете набирать все меньше и меньше кода.
Первоисточник: React — Quick Start — Thinking in React