В прошлой уроке мы узнали что React абстрагирует DOM, обеспечивает высокую производительность, а так же возможность создавать итоговый HTML компонентов на сервере и даже создание нативных интерфейсов для мобильных платформ.
Этот урок посвящен JSX расширению языка JS.
События в React
В React есть своя система событий, которая дает нам согласованность и высокую производительность. Это достигается путем нормализации событий, так что они имеют те же свойства в разных браузерах и платформах. React на самом деле не использует обработчики событий самих узлов. Вместе этого, имеется один слушатель событий прикрепленный к корню документа. Когда событие срабатывает, React перенаправляет его к соответствующему элементу компонента. React также автоматически удаляет обработчики событий когда компонент удаляется.
HTML предоставляет красивый и легкий для понимания интерфейс обработки событий в виде атрибутов тега: onclick, onfocus и т.д. Но с ними есть проблемы, взять хотя бы проблемы кроссбраузерности, загрязнение глобальной области видимости, утечки памяти и т.д.
JSX используется столь же простой в использовании и понимании API, без нежелательных побочных эффектов. Однако есть некоторые отличия от реализации HTML на пример это касается названий событий, они записываются в верблюжьей нотации («OnClick» в место «onclick»).
Поддерживаемые виды событий:
Touch и Click
onTouchStart | onMouseMove | onDragEnd |
onTouchEnd | onMouseEnter | onDragOver |
onTouchMove | onMouseLeave | onDrop |
onTouchCancel | onMouseOut | onContextMenu |
onClick | onDrag | |
onDoubleClick | onDragEnter | |
onMouseDown | onDragLeave | |
onMouseUp | onDragExit | |
onMouseOver | onDragStart |
События клавиатуры
onKeyDown | onKeyUp | onKeyPress |
Фокус и события формы
onFocus | onChange |
onBlur | onInput |
onSubmit |
Другие события
onScroll | onCopy |
onWheel | onCut |
onPaste |
Канбан доска: Управление событиями DOM
В последней итерации приложения канбан, мы добавили стрелочную функцию внутри обработчика OnClick:
... <div className="card" onClick={()=>this.setState({showDetails: !this.state.showDetails})}> ...
В принципе можно так сделать, но это не очень гибкий подход. Давайте изменим эту реализацию на новую, создадим новый метод toggleDetails внутри класса для обработки события:
class Card extends Component { ... toggleDetails() { this.setState({showDetails: !this.state.showDetails}); } render() { ... return ( <div className="card" onClick={this.toggleDetails.bind(this)}>{this.props.title}> <div className="card__title">{this.props.title}</div> {cardDetails} </div> ); } }
Копаем глубже в JSX
JSX в React является расширением JS, в нем используется XML внутри JS кода. Для веб приложений JSX предоставляет набор XML тегов которые очень похожи на HTML теги, но есть и другие случаи, в который другой набор тегов XML используется для описания пользовательского интерфейса, например React SVG, React Canvas и React Native. Когда JSX превращается в обычный JS то XML преобразуется в вызовы соответствующих функций из библиотеки React.
Например из:
... <h1>Hello World</h1> ...
в
... React.createElement("h1", null, "Hello World"); ...
Использование JSX не является обязательным, но предоставляет следующие преимущества:
- XML отлично подходит для представления дерева элементов с атрибутами;
- Код получается более кратким и визуально более понятен;
- Это обычный JS, без изменения семантики языка.
Различия между JSX и HTML
Есть три важных аспекта при написании HTML в JSX:
- Атрибуты тегов пишутся в верблюжий нотации;
- Все элементы должны быть сбалансированы;
- Имена атрибутов основаны на DOM API, а не на HTML спецификации языка.
Давайте рассмотрим каждый пункт поподробнее.
Атрибуты тегов в верблюжий нотации
Например, при описании HTML тега input можно добавить атрибут maxlength:
... <input type="text" maxlength="30" /> ...
В JSX этот атрибут пишется maxLength (обратите внимание что «L» с большой буквы)
... return <input type="text" maxLength="30" /> ...
Все элементы должны быть сбалансированы
Если вы знакомы с XML, то наверняка знаете, что каждый элемент у которое нет зарывающего тега должен быть закрыт: «/>». В HTML есть элементы которые противоречат данным правилам, например <br>. Как вы наверняка уже догадались в JSX такие элементы надо обязательно закрывать: <br/>.
Имена атрибутов основаны на DOM API
Это может сбивать с толку, но это на самом деле очень просто. Атрибуты тега могут отличные названия, от привычных атрибутов в HTML. Одним из таких примеров является class и className. Например в HTML:
... <div id="box" class="some-class"></div> ...
Если вы захотите изменить имя класса с помощью JS, вы могли бы сделать примерно так:
... document.getElementById("box").className="some-other-class"; ...
Как вы заметили в DOM API в замен class используется className. Так как JSX просто расширяет синтаксис JS, атрибуты называются аналогично как определено в DOM API. Например, тот же div может быть реализован на JSX так:
... return <div id="box" className="some-class"></div> ...
Советы по JSX
JSX может быть иногда сложным. В этом разделе небольшие методы и советы для решения общих проблем с которыми вы можете столкнуться при создании компонентов с JSX.
Один корневой узел
React компоненты могут иметь только один корневой элемент. Для того чтобы понять причины такого ограничения, давайте посмотрим на пример возвращения из функции render:
... return( <h1>Hello World</h1> ) ...
В итоге это превращается в инструкцию JS:
... return React.createElement("h1", null, "Hello World"); ...
С другой стороны, следующий код не является верным:
... return ( <h1>Hello World</h1> <h2>Have a nice day</h2> ) ...
Если вы помните в JS оператор return может вернуть только одно значение. А в примере выше мы пытаем вернуть два значения, что не правильно. Что бы вернуть несколько элементов сразу их можно просто обернуть в одни корневой элемент:
... return ( <div> <h1>Hello World</h1> <h2>Have a nice day</h2> </div> ) ...
В итоговом JS это будет выглядеть так:
... return React.createElement("div", null, React.createElement("h1", null, "Hello World"), React.createElement("h2", null, " Have a nice day"), ) ...
Условия в JSX
Конструкцию if не получиться использовать в JSX, что может показаться как ограничением самого JSX, на самом дела является следствием того факта, что JSX это просто JS. Что бы лучше в этом разобраться давайте начнем с анализа того, как JSX трансформируется в обычных JS:
... return ( <div className="salutation">Hello JSX</div> ) ...
в итоге мы получим:
... return ( React.createElement("div", {className: "salutation"}, "Hello JSX"); ) ...
Если мы попытаемся написать if в JSX, например так:
... <div className={if (condition) { "salutation" }}>Hello JSX</div> ...
то получим примерно следующее:
... React.createElement("div", {className: if (condition) { "salutation"}}, "Hello JSX"); ...
и получим ошибку при выполнении:
Какие есть альтернативы?
Несмотря на то, что не возможно использовать «if» внутри JSX, есть альтернативы. Можно например использовать трехкомпонентное (тернарное) выражение.
Например:
... render() { return ( <div className={condition ? "salutation" : ""}> Hello JSX </div> ) } ...
В итоге будет создан корректный JS:
... React.createElement("div", {className: condition ? "salutation" : ""}, "Hello JSX"); ...
Так же это будет работать для условного рендеринга целых узлов:
... <div> {condition ? <span>Hello JSX</span> : null} </div> ...
Перемещение условий
Кроме использования тернарного оператора, есть еще альтернатива в виде перемещения условия за пределы JSX, как это было в сделано для сокрытия и показа деталей Card.
Вместо:
... render() { return ( <div className={if (condition) { "salutation" }}> Hello JSX </div> ) } ...
можно сделать так:
... render() { let className; if(condition){ className = "salutation"; } return ( <div className={className}>Hello JSX</div> ) } ...
React достаточно умен, чтобы правильно обработать пустое значение и даже не будет создан атрибут class если condition === false.
Канбан доска: индикатор открыты или закрыты подробности карты
В первой статье мы использовали технику перемещения условия для переключения деталей карты. Давайте также используем тернарный оператор для добавления индикатора открыты ли подробности или нет.
... class Card extends Component { constructor() {...} toggleDetails() {...} render() { let cardDetails; if (this.state.showDetails) { cardDetails = ( <div className="card__details"> {this.props.description} <CheckList cardId={this.props.id} tasks={this.props.tasks} /> </div> ); } return ( <div className="card"> <div className={ this.state.showDetails ? "card__title card__title--is-open" : "card__title" } onClick={this.toggleDetails.bind(this)} > {this.props.title} </div> {cardDetails} </div> ); } } ...
Пустое пространство
HTML браузере между элементами есть пустое пространство, но в JSX если не указать явно, пустого пространства не будет.
... return ( <div> <a href="http://google.com">Google</a > <a href=“http://facebook.com">Facebook</a> </div> ) ...
в результате
Что бы добавить пустое пространство можно сделать так:
... return( <div> <a href="http://google.com">Google</a>{" "} <a href="http://facebook.com">Facebook</a> </div> ) ...
Комментарии в JSX
Еще одна особенность вытекает из того факта, что JSX не HTML является отсутствие поддержки для комментариев HTML (например, <!— Комментарий —>). Хотя традиционные HTML теги комментарии не поддерживаются, так как JSX это JS, то можно использовать регулярные комментарии JS.
... let content = ( <Nav> {/* child comment, put {} around */} <Person /* multi line comment */ name={window.isLoggedIn ? window.name : ''} // end of line comment /> </Nav> ); ...
Динамический рендеринг HTML
React имеет встроенную защиту от атак XSS, а это значит, что по умолчанию он не позволит динамически генерировать HTML теги. Это все хорошо, но в некоторых случаях вы можете захотеть сгенерировать HMTL на лету. Одним из примеров может быть предоставление данных в формате markdown.
React предоставляет свойство dangerouslySetInnerHTML позволяющее пропустить защиту XSS и сделать что нибудь напрямую.
Канбан доска: Рендеринг Markdown
Давайте посмотрим, как это работает. Добавим изменения в нашу модель данных:
... let cardsList = [ { id: 1, title: "Read the Book", description: "I should read the **whole** book", status: "in-progress", tasks: [] }, { id: 2, title: "Write some code", description: "Code along with the samples in the book. The complete source can be found at [github](https://github.com/pro-react)", status: "todo", tasks: [ { id: 1, name: "ContactList Example", done: true}, { id: 2, name: "Kanban Example", done: false}, { id: 3, name: "My own experiments", done: false} ] } ]; ...
Что бы разметка заработала нам потребуется сторонняя библиотека https://github.com/chjj/marked (npm install —save marked).
... import React, {Component} from 'react'; import CheckList from './CheckList'; import marked from 'marked'; ....
Затем давайте ее используем:
... class Card extends Component { constructor() {...} toggleDetails() {...} render() { let cardDetails; if (this.state.showDetails) { cardDetails = ( <div className="card__details"> {marked(this.props.description)} <CheckList cardId={this.props.id} tasks={this.props.tasks}/> </div> ); } return ( <div className="card"> <div className={ this.state.showDetails ? "card__title card__title--is-open" : "card__title" } onClick={this.toggleDetails.bind(this)} > {this.props.title} </div> {cardDetails} </div> ) } } ...
И в итоге:
Как видно HTML теги не создались. а просто выводятся текстом, что нас явно не устраивает. Давайте это исправим:
... cardDetails = ( <div className="card__details"> <span dangerouslySetInnerHTML={{__html:marked(this.props.description)}} /> <CheckList cardId={this.props.id} tasks={this.props.tasks} /> </div> ) ...
React без JSX
JSX привносит краткий и знаковый синтаксис похожий на HTML для описания пользовательского интерфейса. Тем не менее, можно использовать чистый JS без JSX.
Вы можете создавать теги или компоненты с помощью функции React.createElement, которая принимает имя тега или компонента, свойство объекта и переменное число необязательных аргументов ребенка.
... let child1 = React.createElement('li', null, 'First Text Content'); let child2 = React.createElement('li', null, 'Second Text Content'); let root = React.createElement('ul', { className: 'my-list' }, child1, child2); React.render(root, document.getElementById('example')); ...
Фабрика элементов
Для удобства React предоставляет сокращенные функции React.DOM для обычных HTML-тегов. Давай рассмотрим на более сложном примере:
... React.DOM.form({className:"commentForm"}, React.DOM.input({type:"text", placeholder:"Name"}), React.DOM.input({type:"text", placeholder:"Comment"}), React.DOM.input({type:"submit", value:"Post"}) ) ...
Это эквивалентно следующему:
... <form className="commentForm"> <input type="text" placeholder="Name" /> <input type="text" placeholder="Comment" /> <input type="submit" value="Post" /> </form> ...
На JS ES6 можно создать более краткую форму
import React, { Component } from 'react'; import {render} from 'react-dom'; let { form, input } = React.DOM; class CommentForm extends Component { render(){ return form({className:"commentForm"}, input({type:"text", placeholder:"Name"}), input({type:"text", placeholder:"Comment"}), input({type:"submit", value:"Post"}) ) } } ...
Стили в JSX
Это может показаться странным, но в React можно писать стили на JS и это дает некоторые преимущества:
- Стили без заданной области селекторов;
- Избегает конфликтов спецификации;
- Полный спектр конструкций, переменные, функции и т.д.
Определение строенных стилей
В React компоненты, встроенные стили можно хранить в переменных JS. Названия свойством должны соответствовать DOM API, например node.style.backgroundImage. Пример:
import React, { Component } from 'react'; import {render} from 'react-dom'; class Hello extends Component { render() { let divStyle = { width: 100, height: 30, padding: 5, backgroundColor: '#ee9900' }; return <div style={divStyle}>Hello World</div> } } ...
Канбан доска: Цветная карточка
- Добавим цвет в модель данных.
... let cardsList = [ { id: 1, title: "Read the Book", description: "I should read the **whole** book", color: '#BD8D31', status: "in-progress", tasks: [] }, { id: 2, title: "Write some code", description: "Code along with the samples in the book. The complete source can be found at [github](https://github.com/pro-react)", color: '#3A7E28', status: "todo", tasks: [ { id: 1, name: "ContactList Example", done: true}, { id: 2, name: "Kanban Example", done: false}, { id: 3, name: "My own experiments", done: false} ] } ]; ...
2. Теперь давайте передадим в список
... class List extends Component { render() { var cards = this.props.cards.map((card) => { return <Card id={card.id} title={card.title} description={card.description} color={card.color} tasks={card.tasks} /> }); return ( ... ); } }
3. Создадим новый div и стиль для него
class Card extends Component { constructor() {...} toggleDetails() {...} render() { let cardDetails; if (this.state.showDetails) {...} let sideColor = { position: 'absolute', zIndex: -1, top: 0, bottom: 0, left: 0, width: 7, backgroundColor: this.props.color }; return ( <div className="card"> <div style={sideColor}/> <div className={ this.state.showDetails ? "card__title card__title--is-open" : "card__title" } onClick={this.toggleDetails.bind(this)} > {this.props.title} </div> {cardDetails} </div> ) } }
Работа с формами
В React, внутреннее состояние отображения компонента сведено к минимуму, потому что каждый раз, когда состояние изменяется компонент формируется заново. Цель этого чтобы иметь точное представление состояния компонента. В React есть два способа обработки полей формы: управляемые и неуправляемые компоненты.
Управляемые компоненты
Давайте разберем на примере формы поиска:
import React, { Component } from 'react'; import {render} from 'react-dom'; class Search extends Component { render() { return ( <div> Search Term: <input type="search" value="React" /> </div> ) } } render(<Search />, document.body);
В итоге у нас получится поле поиска с неизменяемым значением «React»
Это мягко говоря бесполезный поиск. Чтобы поправить данное положение, давайте добавим изменяемое значение.
class Search extends Component { constructor() { super(); this.state = { searchTerm: "React" }; } render() { return ( <div> Search Term: <input type="search" value={this.state.searchTerm} /> </div> ) } }
И добавим событие для обновления состояния:
... class Search extends Component { constructor() { super(); this.state = { searchTerm: "React" }; } handleChange(event) { this.setState({searchTerm: event.target.value}); } render() { return ( <div> Search Term: <input type="search" value={this.state.searchTerm} onChange={this.handleChange.bind(this)} /> </div> ) } } ...
Это выглядеть немного запутано, но имеет некоторые преимущества:
- Состояние изменяется через интерфейс и полностью управляется в коде JS;
- Эта модель позволяет легко реализовать проверку вводимых данных.
Например можно ограничить ввод 50 символами:
... this.setState({searchTerm: event.target.value.substr(0, 50)}); ...
Особые случаи
Есть несколько особых случаев: textarea и select
textarea
В HTML содержимое обычно устанавливается между тегами textarea.
... <textarea>This is the description.</textarea> ...
Что легко позволяет создавать многострочный текст, однако в React лучше использовать атрибут value и что бы сделать перенос строки достаточно использовать «\n».
... <textarea value="This is a description." /> ....
select
В HTML, что бы узнать выбранный элемент из списка используется атрибут «selected». Как вы наверно догадались в JSX используется «value»:
... <select value="B"> <option value="A">Mobile</option> <option value="B">Work</option> <option value="C">Home</option> </select> ...
Неуправляемые компоненты
Управляемые компоненты придерживаются принципов заложеныx в самом React. В то время как неуправляемые компоненты являются анти-паттерном. Но вам может не требоваться такой жесткий контроль каждого поля на форме, а например только получения всех данных формы после полного заполнения ее пользователем.
Любое поле на форме без атрибута value является неуправляемым компонентом. Например:
... return ( <form> <div className="formGroup"> Name: <input name="name" type="text" /> </div> <div className="formGroup"> E-mail: <input name="email" type="mail" /> </div> <button type="submit">Submit</button> </form> ) ...
Тут мы имеем два пустых input и можем без проблем туда что либо ввести. Если вы хотите установить начальное значение для неуправляемого поля используйте defaultValue взамен value. С помощью события onSubmit мы можем обрабатывать такие формы, например так:
... handleSubmit(event) { console.log("Submitted values are: ", event.target.name.value, event.target.email.value); event.preventDefault(); } render() { return ( <form onSubmit={this.handleSubmit}> <div className="formGroup"> Name: <input name="name" type="text" /> </div> <div className="formGroup"> E-mail: <input name="email" type="mail" /> </div> <button type="submit">Submit</button> </form> ) } ...
Канбан доска: Форма создания задачи
Давайте добавим неуправляемое текстовое поле для добавления задач.
... class CheckList extends Component { render() { let tasks = this.props.tasks.map((task) => (...)); return ( <div className="checklist"> <ul>{tasks}</ul> <input type="text" className="checklist--add-task" placeholder="Type then hit Enter to add a task" /> </div> ); } } ...
Так как мы не указали атрибут value пользователь может свободно заполнять это поле. В следующем уроке добавим добавление в список задач. А пока давайте добавим немного СSS:
... .checklist--add-task { border: 1px dashed #bbb; width: 100%; padding: 10px; margin-top: 5px; border-radius: 3px; }
Виртуальный DOM под капотом
Как вы наверняка помните один и ключевых особенностей React является возможность изменят только те DOM элементы которые реальные изменились. Все это благодаря легковесному виртуальному DOM дереву. Такой алгоритм очень сложен и может быть дорогостоящей операцией. Что бы сделать его более быстрым, React делает предположения о том, как может работать типичное приложение, что позволяется еще больше ускорить работу алгоритма. Некоторые предположения включают:
- При сравнении узлов DOM деревьев, если узлы разных типов (например, был div стал span), React будет рассматривать их как два разных субъекта дерева: выбросит первый и построит/вставит второй.
- Так же логика работает для пользовательских компонентов. Если они не имеют одного и того же типа, React не собирается даже пытаться смотреть что они делают. Просто удалит первый вставить второй.
- Если узлы имеют одинаковый тип в React имеется два способа обработки на такой случай:
- Если это DOM-элемент и например <div id=»before» /> изменится до <div id=»after» /> React изменит только атрибуты и стили без пересоздания элемента в DOM дереве.
- Если это пользовательский компонент например изменился <Contact details={false} /> до <Contact details={true} /> React не заменит компонент. Вместо этого он будет передавать новые значения свойств текущему компоненту, этот процесс будет закончен с вызовом функции render данного компонента.
Ключи
Хотя алгоритмы поиска различий виртуального DOM и реального достаточно умны и быстры. React имеет некоторые допущения особенно при построении списков. Давай рассмотрим их на примере списка до и после изменений:
... <li>Orange</li> <li>Banana</li> ...
... <li>Apple</li> <li>Orange</li> ...
Разница между этими двумя списка кажется очевидной, но что является лучшим подходом к трансформации первого списка во второй? Например тут возможно добавление нового элемента Apple и удаление последнего, но возможно и изменение имени последнего элемента и его положения, а также другие варианты развития событий. Из за того что трудно определить что же произошло и какие компоненты изменились точно, могут появляться побочные эффекты, плюс в больших спискам из-за этого может страдать производительность. Чтобы этого избежать в React ввели понятие ключей, уникальных идентификаторов элементов в списке, что позволяет быстро искать элементы и применять к ним изменения без побочных эффектов.
Канбан доска: Ключи
Если посмотреть в консоль нашего приложение можно увидеть, что React нас уже предупреждает, что мы к сожалению не используем ключи.
Ключом может быть любое уникальное значение, которое является уникальным и постоянным. В наших данных есть подходящий кандидат — id карточки. Давайте его и используем:
... class List extends Component { render() { var cards = this.props.cards.map((card) => { return <Card key={card.id} id={card.id} title={card.title} description={card.description} color={card.color} tasks={card.tasks} /> }); return (...); } } ...
Давайте также добавим ключи для списка задач:
... class CheckList extends Component { render() { let tasks = this.props.tasks.map((task) => ( <li key={task.id} className="checklist__task"> <input type="checkbox" defaultChecked={task.done}/> {task.name} <a href="#" className="checklist__task--remove"/> </li> )); return (...); } } ...
Refs
При работе с React вы всегда имеете дело с виртуальным DOM. Но в некоторых случаях вам может понадобиться «достучаться» до фактической DOM разметки. В React есть такая возможность называемая refs. Но подумайте дважды перед применением этого метода. Давайте разберем его на примере:
... <input ref="myInput" /> ...
Тут мы пометили некое текстовое поле и теперь мы можем в коде его использовать через свойство this.refs например так:
... let input = this.refs.myInput; let inputValue = input.value; let inputRect = input.getBoundingClientRect(); ...
Походу этих уроков мы редко будем пользоваться этой возможностью. Давайте рассмотрим еще один пример, когда использование refs оправдано:
... class FocusText extends Component { handleClick() { // Explicitly focus the text input using the raw DOM API. this.refs.myTextInput.focus(); } render() { // The ref attribute adds a reference to the component to // this.refs when the component is mounted. return ( <div> <input type="text" ref="myTextInput" /> <input type="button" value="Focus the text input" onClick={this.handleClick.bind(this)} /> </div> ); } } ...
Тут у нас компонент, который представляет собой текстовое поле и кнопку и по нажатию на которую полю передается фокус.
Итоги
В этом уроке мы узнали много нового о React DOM абстракции. Рассмотрели события, реквизиты, встроенные стили, работу с формами и другие нюансы работы React.