КатегорииReact

React урок 2 Внутри абстракции DOM

В прошлой уроке мы узнали что 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");
...

и получим ошибку при выполнении:

error_if

Какие есть альтернативы?

Несмотря на то, что не возможно использовать «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>
        );
    }
}
...

indicator

Пустое пространство 

HTML браузере между элементами есть пустое пространство, но в JSX если не указать явно, пустого пространства не будет.

...
return (
  <div>
    <a href="http://google.com">Google</a >
    <a href=“http://facebook.com">Facebook</a>
  </div>
)
...

в результате

nospace

Что бы добавить пустое пространство можно сделать так:

...
return(
  <div>
    <a href="http://google.com">Google</a>{" "}
    <a href="http://facebook.com">Facebook</a>
  </div>
)
...

space

Комментарии в 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>
        )
    }
}
...

И в итоге:

markdown

Как видно HTML теги не создались. а просто выводятся текстом, что нас явно не устраивает. Давайте это исправим:

...
cardDetails = (
  <div className="card__details">
    <span dangerouslySetInnerHTML={{__html:marked(this.props.description)}} />
    <CheckList cardId={this.props.id} tasks={this.props.tasks} />
  </div>
)
...

mark

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>
}
}
...

Канбан доска: Цветная карточка

  1. Добавим цвет в модель данных.
...
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>
        )
    }
}

result_style

Работа с формами

В 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»

search

Это мягко говоря бесполезный поиск. Чтобы поправить данное положение, давайте добавим изменяемое значение.

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 имеется два способа обработки на такой случай:
  1. Если это DOM-элемент и например <div id=»before» /> изменится до <div id=»after» /> React изменит только атрибуты и стили без пересоздания элемента в DOM дереве.
  2. Если это пользовательский компонент например изменился <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 нас уже предупреждает, что мы к сожалению не используем ключи.

console_key

Ключом может быть любое уникальное значение, которое является уникальным и постоянным. В наших данных есть подходящий кандидат — 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.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *