React — это новый ребенок в блоке, а это значит, что не многие люди имеют реальный опыт создания чего-то с ним. Эта статья будет посвящена состоянию компонентов и времени их использования.
Пример будет использован в качестве основы для нашего исследования. Простой блог со списком категорий, который при нажатии отображает список статей. Для начала данные будут жестко закодированы, а позже мы будем использовать Socket.IO для имитации публикации внешних статей.
Дети без гражданства, родители с состоянием
Давайте начнем эту статью с цитирования документации React по этой теме:
Обычным шаблоном является создание нескольких компонентов без сохранения состояния, которые просто визуализируют данные и имеют над ними компонент с отслеживанием состояния в иерархии, которая передает свое состояние своим дочерним элементам через
props
.
Как мы начнем реализовывать этот шаблон? Другими словами, шаблон включает иерархию родительских и дочерних компонентов.
Каждый компонент будет в отдельном файле для повышения модульности. Мы будем использовать Browserify для:
- доставить один пакетный файл JavaScript в браузер
- предотвратить глобальное загрязнение пространства имен (например, на объекте
window
в случае браузера) - поддержка модулей CommonJS (то есть
module.exports
которые мы видим в коде Node.js)
Давайте начнем наш пример с рассмотрения нижней части иерархии с определения идеальных кандидатов для дочерних компонентов без сохранения состояния.
Определите дочерние компоненты без состояния
Как я описал ранее, в примере есть два списка: категории и статьи. В нашем приложении классы для этих списков будут называться CategoryList
и ArticleList
соответственно. Оба они являются хорошими кандидатами на роль дочернего компонента.
categoryList.jsx
, файл, содержащий CategoryList
, содержит следующий код:
var React = require('react'); var CategoryList = React.createClass({ render: function() { return ( <ul> {this.props.categories.map(function(category) { return ( <li key={category.id} onClick={this.props.onCategorySelected.bind(null, category.id)}> {category.title} </li> ); }, this)} </ul> ); } }); module.exports = CategoryList;
Этот компонент, как и все остальные, написан с использованием JSX. Это расширение JavaScript, которое позволяет вставлять XML-разметку. Вы можете узнать больше об этом на странице документации React .
articleList.jsx
, файл, содержащий ArticleList
, содержит следующий код:
var React = require('react'); var ArticleList = React.createClass({ render: function() { return ( <ul> {this.props.articles.map(function(article) { return ( <li key={article.id}> {article.title + ' by ' + article.author} </li> ); })} </ul> ); } }); module.exports = ArticleList;
Вы заметите, что ни CategoryList
ни ArticleList
не ArticleList
доступа в их методе render
или они не реализуют getInitialState()
Мы следуем шаблону, предложенному в документации, и передаем данные от родителя через props
.
Важно отметить, что эти компоненты полностью отделены. ArticleList
может быть передан массив статей любым родителем. Например, ArticleList
может быть повторно использован без изменений в контексте, сгруппированном по авторам, а не в контексте, сгруппированном по категориям.
Теперь, когда у нас есть дочерние компоненты без состояния, нам нужно подняться на уровень в иерархии и создать родительский компонент с состоянием.
Создайте родительский компонент с сохранением состояния
Родительский компонент с состоянием может находиться на любом уровне в иерархии компонентов, то есть он также может быть дочерним по отношению к другим компонентам. Это не должен быть самый верхний компонент (компонент, переданный в React.render()
). Однако в этом случае, поскольку пример является относительно простым, наш родительский элемент с состоянием также является самым верхним компонентом.
Мы назовем этот компонент Blog
и blog.jsx
его в файл с именем blog.jsx
. Последний содержит следующий код:
var React = require('react'); var CategoryList = require('./categoryList.jsx'); var ArticleList = require('./articleList.jsx'); var Blog = React.createClass({ getInitialState: function() { var categories = [ { id: 1, title: 'AngularJS' }, { id: 2, title: 'React' } ]; return { categories: categories, selectedCategoryArticles: this.getCategoryArticles(this.props.defaultCategoryId) }; }, getCategoryArticles: function(categoryId) { var articles = [ { id: 1, categoryId: 1, title: 'Managing Client Only State in AngularJS', author: 'M Godfrey' }, { id: 2, categoryId: 1, title: 'The Best Way to Share Data Between AngularJS Controllers', author: 'M Godfrey' }, { id: 3, categoryId: 2, title: 'Demystifying React Component State', author: 'M Godfrey' } ]; return articles.filter(function(article) { return article.categoryId === categoryId; }); }, render: function() { return ( <div> <CategoryList categories={this.state.categories} onCategorySelected={this._onCategorySelected} /> <ArticleList articles={this.state.selectedCategoryArticles} /> </div> ); }, _onCategorySelected: function(categoryId) { this.setState({ selectedCategoryArticles: this.getCategoryArticles(categoryId) }); } }); module.exports = Blog;
Код выше достаточно многословен. Это связано с getInitialState()
articles
и categories
в getInitialState()
и getCategoryArticles()
соответственно. В начале статьи я упомянул, что данные будут изначально жестко закодированы, но позже предоставлены Socket.IO. Так что терпите меня, так как решение скоро станет более интересным.
Теперь у нас есть два дочерних компонента и один родительский компонент. Однако этого недостаточно для полностью рабочего решения. Для этого нам понадобятся еще два файла: скрипт для начальной загрузки компонента Blog
и HTML-страница для его отображения.
app.jsx
, файл с кодом для начальной загрузки демо-версии, содержит следующий код:
var React = require('react'); var Blog = require('./blog.jsx'); React.render( <Blog defaultCategoryId="1" />, document.getElementById('blogContainer') );
Наконец, наша HTML-страница с именем index.html
содержит следующую разметку:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>Demystifying react-component state</title> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link href="styles.css" rel="stylesheet" /> </head> <body> <h1>Demystifying React Component State</h1> <div id="blogContainer"></div> <script src="bundle.js"></script> </body> </html>
Вы заметите, что index.html
не загружает app.jsx
. Это где Browserify вступает в игру. Прежде чем вы сможете использовать приложение, вы должны выполнить следующую команду:
browserify -t reactify browser/app.jsx -o browser/bundle.js
Browserify начинается с app.jsx
и следует за всеми вызовами app.jsx
require()
для вывода bundle.js
. bundle.js
будет содержать наши три компонента, app.jsx
и саму библиотеку React, все в одном закрытии, чтобы предотвратить глобальное загрязнение пространства имен.
Вот демонстрация полностью рабочего решения.
улучшения
До этого момента эта статья была сосредоточена на реализации шаблона дочерних компонентов без сохранения состояния и родительских компонентов с сохранением состояния, как предлагается в документации React . Существуют ли другие области документации, которые могут помочь нам улучшить наш код?
В следующих разделах мы рассмотрим два из них. Первый будет использовать обработчики событий, а второй — вычисленные данные.
Пусть обработчики событий управляют содержанием
Документация React предлагает:
Состояние должно содержать данные, которые обработчик событий компонента может изменить, чтобы вызвать обновление пользовательского интерфейса.
В нашем решении метод _onCategorySelected
компонента Blog
является единственным обработчиком событий, который меняет только state.selectedCategoryArticles
. По этой причине state.categories
и state.articles
не должны существовать.
Мы можем исправить это, передав categories
и articles
в app.jsx
в React.render()
вместе с defaultCategoryId
следующим образом:
var React = require('react'); var Blog = require('./blog.jsx'); var categories = [ { id: 1, title: 'AngularJS' }, { id: 2, title: 'React' } ]; var articles = [ { id: 1, categoryId: 1, title: 'Managing Client Only State in AngularJS', author: 'M Godfrey' }, { id: 2, categoryId: 1, title: 'The Best Way to Share Data Between AngularJS Controllers', author: 'M Godfrey' }, { id: 3, categoryId: 2, title: 'Demystifying React Component State', author: 'M Godfrey' } ]; React.render( <Blog defaultCategoryId="1" articles={articles} categories={categories} />, document.getElementById('blogContainer') );
В blog.jsx
мы теперь получаем доступ к статьям и категориям из props
следующим образом:
var React = require('react'); var CategoryList = require('./categoryList.jsx'); var ArticleList = require('./articleList.jsx'); var Blog = React.createClass({ getInitialState: function() { return { selectedCategoryArticles: this.getCategoryArticles(this.props.defaultCategoryId) }; }, getCategoryArticles: function(categoryId) { return this.props.articles.filter(function(article) { return article.categoryId === categoryId; }); }, render: function() { return ( <div> <CategoryList categories={this.props.categories} onCategorySelected={this._onCategorySelected} /> <ArticleList articles={this.state.selectedCategoryArticles} /> </div> ); }, _onCategorySelected: function(categoryId) { this.setState({ selectedCategoryArticles: this.getCategoryArticles(categoryId) }); } }); module.exports = Blog;
Второе улучшение, которое мы рассмотрим, — это вычисленные данные.
Вычисленные данные
Документация React дополнительно описывает:
this.state
должен содержать только минимальный объем данных, необходимый для представления состояния вашего пользовательского интерфейса.
state.selectedCategoryArticles
компонента state.selectedCategoryArticles
состоит из вычисленных данных. В документации рекомендуется, чтобы все вычисления были записаны в методе render
компонента. Мы можем добиться этого, изменив blog.jsx
следующим образом (сообщается только о методе render()
):
render: function() { var selectedCategoryArticles = this.props.articles.filter(function(article) { return article.categoryId === this.state.selectedCategoryId; }, this); return ( <div> <CategoryList categories={this.props.categories} onCategorySelected={this._onCategorySelected} /> <ArticleList articles={selectedCategoryArticles} /> </div> ); }
Несмотря на то, что это простой совет для нашего простого примера, рассмотрите количество статей, опубликованных SitePoint. Фильтр массива в render()
может стать очень дорогим. Для этого сценария я бы рассмотрел изменение модели, введя свойство массива articles
в каждой category
.
Последнее предложение завершает наш анализ и реализацию рекомендаций по документации React. Но у нас есть одно последнее изменение, чтобы выполнить …
Внешние обновления
Мы смоделируем публикацию статьи с помощью Socket.IO . Я опущу код сервера для краткости.
На странице API компонента документация React описывает:
Единственный способ получить дескриптор экземпляра компонента React вне React — сохранить возвращаемое значение React.render.
С этим знанием интеграция Socket.IO становится тривиальной.
app.jsx
теперь включает создание клиента SocketIO, который прослушивает сообщения articlePublished
с сервера следующим образом (я просто покажу новый код):
var React = require('react'); var Blog = require('./blog.jsx'); var categories = [ { id: 1, title: 'AngularJS' }, { id: 2, title: 'React' } ]; var articles = [ { id: 1, categoryId: 1, title: 'Managing Client Only State in AngularJS', author: 'M Godfrey' }, { id: 2, categoryId: 1, title: 'The Best Way to Share Data Between AngularJS Controllers', author: 'M Godfrey' }, { id: 3, categoryId: 2, title: 'Demystifying React Component State', author: 'M Godfrey' } ]; var renderedBlog = React.render( <Blog initialCategoryId="1" initialArticles={articles} categories={categories} />, document.getElementById('blogContainer') ); var socket = require('socket.io-client')('http://localhost:8000/'); socket.on('articlePublished', function(article) { renderedBlog._onArticlePublished(article); });
blog.jsx
изменяется в последний раз, предоставляя дополнительный обработчик событий следующим образом:
var React = require('react'); var CategoryList = require('./categoryList.jsx'); var ArticleList = require('./articleList.jsx'); var Blog = React.createClass({ getInitialState: function() { return { articles: this.props.initialArticles, selectedCategoryId: this.props.initialCategoryId }; }, render: function() { var selectedCategoryArticles = this.state.articles.filter(function(article) { return article.categoryId === this.state.selectedCategoryId; }, this); return ( <div> <CategoryList categories={this.props.categories} onCategorySelected={this._onCategorySelected} /> <ArticleList articles={selectedCategoryArticles} /> </div> ); }, _onCategorySelected: function(categoryId) { this.setState({ selectedCategoryId: categoryId }); }, _onArticlePublished: function(article) { // we should treat state as immutable // create a new array by concatenating new and old contents // http://stackoverflow.com/a/26254086/305844 this.setState({ articles: this.state.articles.concat([article]) }); } }); module.exports = Blog;
Вы заметите, что state.articles
был введен снова. Из-за этого я ввел «начальные» имена переменных в props
чтобы передать их истинное намерение.
Вот демонстрация окончательного рабочего решения. Как видите, сервер публикует статьи только для категории AngularJS и «творчески» использует метку времени для каждого заголовка статьи.
Вывод
Документация React очень обширна, и вы можете многому научиться у нее. Написание этой статьи заставило меня следовать и точно применять ее раздел. Реальные приложения, скорее всего, заставят нас отклониться от него. Когда мы сталкиваемся с этими сценариями, нам, возможно, следует стремиться изменить другие компоненты приложения (например, модель или структуру представления). Я хотел бы услышать ваши мысли в комментариях.
Полностью рабочий пример, включая код сервера Socket.IO, можно найти в моей учетной записи GitHub .
Если вы пытаетесь улучшить свою игру React, ознакомьтесь с нашим примером видео из нашего мини-курса « Быстрое начало работы» , доступного для членов SitePoint. Изучите основные и практические части React с отличным практическим опытом создания компонентов React с нуля.
Press shift question mark to access a list of keyboard shortcuts