Статьи

Демистификация состояния компонентов реакции

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 компонент состояния 1

улучшения

До этого момента эта статья была сосредоточена на реализации шаблона дочерних компонентов без сохранения состояния и родительских компонентов с сохранением состояния, как предлагается в документации 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 с нуля.