Рассылка
Подпишитесь на рассылку и получайте дайджест новостей и статей.
Никакого спама!
Подписаться

Справочник ReactJS

Урок

Мы создадим простой, но реалистичный блок комментариев, который можно добавить в блог, по аналогии с такими реал-тайм модулями комментирования как Disqus, LiveFyre или комментарии Facebook.

Наш компонент будет предоставлять:

  • Список комментариев
  • Форму отправки комментария
  • Удобный способ подключить бэкенд

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

  • Оптимистичное комментирование: комментарий будет появляться в списке до того, как он будет сохранен на сервере.
  • Живое обновление: комментарии других пользователей будут появляться в списке в реальном времени.
  • Поддержка markdown: пользователи смогут использовать markdown для форматирования текста.

Хотите пропустить все и сразу посмотреть код?

Он есть на GitHub.

Запуск сервера

Для того, чтобы начать этот урок, нам потребуется сервер. Он должен предоставлять API, который мы будем использовать для получения и сохранения данных. Для того, чтобы сделать это как можно проще, мы создали простой сервер на нескольких языках программирования, который делает именно то, что нам нужно. Вы можете посмотреть исходный код или загрузить архив, содержащий все необходимое для начала работы.

Для простоты, наш сервер использует JSON-файл в качестве базы даных. Такой пример не подойдет для production-среды, но он эмулирует работу API. После того, как вы запустите сервер, он будет обрабатывать запросы к API, а также отдавать статические страницы.

Начало

В этом уроке мы будем делать все как можно проще. Откройте public/index.html в вашем любимом редакторе. Его содержимое должно выглядеть примерно так (возможно, с некоторыми незначительными отличиями, мы добавим дополнительный тег позже):


<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>React Tutorial</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.3/react.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.3/react-dom.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
  </head>
  <body>
    <div id="content"></div>
    <script type="text/babel" src="scripts/example.js"></script>
    <script type="text/babel">
      // To get started with this tutorial running your own code, simply remove
      // the script tag loading scripts/example.js and start writing code here.
    </script>
  </body>
</html>

В ходе урока мы будем писать JavaScript-код внутри тега <script>. Мы не настраивали автоматическое обновление браузера, поэтому нужно будет обновлять браузер, чтобы посмотреть изменения. Откройте страницу http://localhost:3000 (после запуска сервера). Вы увидите готовый продукт, который мы будем разрабатывать. Если вы готовы начать, просто удалите содержимое тега <script> и продолжим.

Обратите внимание:
Мы подключили jQuery для упрощения ajax-вызовов, но это НЕ обязательно для работы React.

Ваш первый компонент

В основе React лежат модульные, переиспользуемые компоненты. Для блока комментариев у нас будет следующая структура компонентов:

— CommentBox   — CommentList     — Comment   — CommentForm

Давайте создадим компонент CommentBox, который будет представлять собой простой <div>:


// tutorial1.js
var CommentBox = React.createClass({
    render: function() {
        return (
            <div className="commentBox">
                Hello, world! I am a CommentBox.
            </div>
        );
    }
});
ReactDOM.render(
    <CommentBox />,
    document.getElementById('content')
);

Обратите внимание, что стандартные HTML теги пишутся с маленькой буквы, тогда как компоненты React пишутся с большой буквы.

Синтаксис JSX

Первое, на что следует обратить внимание, это XML-синтаксис в JavaScript-коде. У нас есть простой прекомпилятор, который транслирует этот синтаксический сахар в JavaScript:


// tutorial1-raw.js
var CommentBox = React.createClass({displayName: 'CommentBox',
  render: function() {
    return (
      React.createElement('div', {className: "commentBox"},
        "Hello, world! I am a CommentBox."
      )
    );
  }
});
ReactDOM.render(
  React.createElement(CommentBox, null),
  document.getElementById('content')
);

Его использование необязательно, но нам синтаксис JSX кажется проще, чем JavaScript. Подробнее о нем вы можете прочитать в статье Синтаксис JSX.

Что происходит

Мы передаем некоторые методы в JavaScript объекте в функцию React.createClass() для создания нового компонента. Самым важным из них является метод render, который возвращает дерево компонентов, которые в конечном итоге будут отрисованы в HTML.

Тег <div> — это не фактический узел DOM; теги инициализируют компонент React. Вы можете думать о них как о маркерах или части данных, которые обрабатывает React. React безопасен. Мы не генерируем HTML строк, поэтому защита от XSS включена по умолчанию.

Вы не должны обязательно возвращать HTML. Вы можете вернуть дерево компонентов. Это делает компонеты React комбинируемыми: это ключевой принцип удобного для поддержки фронтенд-приложения.

Метод ReactDOM.render() инициализирует корневой компонент, стартует фреймворк и вставляет разметку в DOM элемент, переданный во втором аргументе.

ReactDOM предоставляет методы, специфичные для DOM, в то время как React содержит ядро, которое необходимо для работы на разных платформах (например, React Native).

Композиция компонентов

Давайте создадим заготовки для компонентов CommentList и CommentForm, которые, опять же, будут простыми <div>-ами. Добавим эти два компонента в наш файл рядом с компонентом CommentBox:


// tutorial2.js
var CommentList = React.createClass({
  render: function() {
    return (
      <div className="commentList">
        Hello, world! I am a CommentList.
      </div>
    );
  }
});

var CommentForm = React.createClass({
  render: function() {
    return (
      <div className="commentForm">
        Hello, world! I am a CommentForm.
      </div>
    );
  }
});

Далее, обновим компонент CommentBox так, чтобы он использовал новые компонеты:


// tutorial3.js
var CommentBox = React.createClass({
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList />
        <CommentForm />
      </div>
    );
  }
});

Обратите внимание на то, как мы смешиваем HTML-теги и компоненты, которые мы создали. Компоненты HTML — это обычные компоненты React, с одним отличием. Компилятор JSX автоматически заменяет HTML-теги на выражение React.createElement(tagName) и оставляет все остальное. Это сделано для предотвращения загрязнения глобального пространства имен.

Использование свойств

Давайте создадим компонент Comment, который будет зависеть от данных, передаваемых от родителя. Данные, передаваемые от родительского компонента, доступны в виде «свойств» дочернего компонента. Эти свойства доступны через this.props. Используя свойства, мы можем прочесть данные, переданные в Comment от CommentList, и отобразить их:


// tutorial4.js
var Comment = React.createClass({
  render: function() {
    return (
      <div className="comment">
        <h2 className="commentAuthor">
          {this.props.author}
        </h2>
        {this.props.children}
      </div>
    );
  }
});

В фигурных скобках внутри JSX можно использовать выражения JavaScript, текст или компоненты React. Атрибуты компонента можно получить через свойства объекта this.props, а все содержимое — через this.props.children.

Свойства компонента

Теперь, когда мы определили компонент Comment, мы можем передать ему автора и текст комментария. Это позволяет повторно использовать один код для каждого комментария. Добавим несколько комментариев в компонент CommentList:


// tutorial5.js
var CommentList = React.createClass({
  render: function() {
    return (
      <div className="commentList">
        <Comment author="Pete Hunt">This is one comment</Comment>
        <Comment author="Jordan Walke">This is *another* comment</Comment>
      </div>
    );
  }
});

Обратите внимание, как мы передаем некоторые данные от родительского компонена в дочерние. Например, мы передали Pete Hunt (как атрибут) и This is one comment (как вложенный узел) в первый компонент Comment. Компонент Comment может получить значения этих свойств через свойства this.props.author и this.props.children.

Добавление поддержки Markdown

Markdown — это простой способ форматирования текста. Например, если обернуть текст в звездочки, он будет отображаться курсивом.

Во-первых, нам нужно подключить в приложение библиотеку marked. Это JavaScript-библиотека, которая принимает Markdown-разметку и конвертирует в HTML. Для этого мы добавили тег <script> в head:


<!-- index.html -->
<head>
  <meta charset="utf-8" />
  <title>React Tutorial</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.3/react.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.3/react-dom.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/0.3.2/marked.min.js"></script>
</head>

Далее, сконвертируем текст комментария и выведем его:


// tutorial6.js
var Comment = React.createClass({
  render: function() {
    return (
      <div className="comment">
        <h2 className="commentAuthor">
          {this.props.author}
        </h2>
        {marked(this.props.children.toString())}
      </div>
    );
  }
});

Здесь мы просто вызываем функцию marked. Нам нужно сконвертировать значение this.props.children из обертки React в строку, поэтому мы вызвали метод toString().

Но есть проблема! Комментарий отображается в браузере так: "<p>This is<em>another</em> comment</p>". Мы хотим, чтобы теги отображались как HTML.

Так React защищает вас от XSS атак. Существует способ обойти эту защиту, но фреймворк предупреждает вас, не использовать его:


// tutorial7.js
var Comment = React.createClass({
  rawMarkup: function() {
    var rawMarkup = marked(this.props.children.toString(), {sanitize: true});
    return { __html: rawMarkup };
  },

  render: function() {
    return (
      <div className="comment">
        <h2 className="commentAuthor">
          {this.props.author}
        </h2>
        <span dangerouslySetInnerHTML={this.rawMarkup()} />
      </div>
    );
  }
});

Это специальный API, который затрудняет вставку HTML, но для функции marked мы использовали хак.

Запомните: чтобы использовать эту возможность, вы должны быть уверены в безопасности вставляемого текста. В нашем случае мы передаем опцию sanitize: true, которая говорит, что HTML нужно вернуть в исходном виде.

Подключение модели данных

До этих пор мы вставляли комментарии непосредственно в исходном коде. Вместо этого, давайте отобразим в списке комментариев JSON-данные. Они будут приходит от сервера, но пока зададим их в исходном коде:


// tutorial8.js
var data = [
    {id: 1, author: "Pete Hunt", text: "This is one comment"},
    {id: 2, author: "Jordan Walke", text: "This is *another* comment"}
];

Нам необходимо передать эти данные в компонент CommentList. Изменим компонент CommentBox и вызов ReactDOM.render(), чтобы передать данные в компонент CommentList в качестве свойства:


// tutorial9.js
var CommentBox = React.createClass({
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.props.data} />
        <CommentForm />
      </div>
    );
  }
});

ReactDOM.render(
  <CommentBox data={data} />,
  document.getElementById('content')
);

Теперь, когда данные доступны в CommentList, отобразим список комментариев динамически:


// tutorial10.js
var CommentList = React.createClass({
  render: function() {
    var commentNodes = this.props.data.map(function(comment) {
      return (
        <Comment author={comment.author} key={comment.id}>
          {comment.text}
        </Comment>
      );
    });
    return (
      <div className="commentList">
        {commentNodes}
      </div>
    );
  }
});

Это все!

Получение данных от сервера

Теперь заменим статичные данные на некие динамические данные, возвращаемые сервером. Удалим атрибут компонента data и добавим URL:


// tutorial11.js
ReactDOM.render(
  <CommentBox url="/api/comments" />,
  document.getElementById('content')
);

Этот компонент отличается от предыдущих компонентов, потому что он будет перерисовывать себя. Компонент не будет иметь каких-либо данных, пока не вернется запрос от сервера, и в этот момент компонент должен отобразить новые комментарии.

Примечание: код не будет работать на этом этапе.

Реактивное состояние

Компоненты, основанные на свойствах, отрисовывают себя один раз. Свойство компонента props является неизменяемым: оно передается в компонент от родителя и «владеет» им тоже родитель. Для реализации взаимодествия, существует изменяемое свойство компонента state. this.state является приватным для компонента и может быть изменено с помощью метода this.setState(). При изменении состояния, компонент перерисовывает себя.

Методы render() являются функциями от this.props и this.state. Фреймворк гарантирует, что UI всегда отображает корректные данные.

При получении данных от сервера, мы будем изменять комментарии. Давайте добавим поле data в состояние компонента CommentBox:


// tutorial12.js
var CommentBox = React.createClass({
  getInitialState: function() {
    return {data: []};
  },
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm />
      </div>
    );
  }
});

Метод getInitialState() вызывается один раз в течении жизненного цикла компонента и задает его начальное состояние.

Обновление состояния

Мы хотим, чтобы при создании компонента отправлялся запрос на сервер и состояние компонента обновлялось полученными данными. Мы собираемся использовать jQuery для отправки запроса. Данные уже есть на сервере (в файле comments.json), после получения данных this.state.data должен выглядеть так:


[
  {"author": "Pete Hunt", "text": "This is one comment"},
  {"author": "Jordan Walke", "text": "This is *another* comment"}
]

// tutorial13.js
var CommentBox = React.createClass({
  getInitialState: function() {
    return {data: []};
  },
  componentDidMount: function() {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      cache: false,
      success: function(data) {
        this.setState({data: data});
      }.bind(this),
      error: function(xhr, status, err) {
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    });
  },
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm />
      </div>
    );
  }
});

В данном примере, метод componentDidMount вызовется автоматически после первой отрисовки компонента. Ключевым моментом является вызов this.setState(). Мы заменяем старое значение массива data на данные, пришедшие от сервера, и UI автоматически обновляется. Поскольку состояние является реактивным, достаточно небольшого изменения, чтобы получить живое обновление. Мы будем исользовать простые периодические запросы, но вы можете использовать вебсокеты или другие технологии.


// tutorial14.js
var CommentBox = React.createClass({
  loadCommentsFromServer: function() {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      cache: false,
      success: function(data) {
        this.setState({data: data});
      }.bind(this),
      error: function(xhr, status, err) {
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    });
  },
  getInitialState: function() {
    return {data: []};
  },
  componentDidMount: function() {
    this.loadCommentsFromServer();
    setInterval(this.loadCommentsFromServer, this.props.pollInterval);
  },
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm />
      </div>
    );
  }
});

ReactDOM.render(
  <CommentBox url="/api/comments" pollInterval={2000} />,
  document.getElementById('content')
);

Все, что мы сделали — переместили AJAX вызов в отдельный метод, вызвали первый раз после отрисовки компонента и периодически каждые две секунды. Попробуйте запустить этот пример в браузере и изменить файл comments.json (в директории с сервером); через 2 секунды изменения отобразятся!

Добавление новых комментариев

Пришло время для создания формы. Наш компонент CommentForm должен спрашивать имя пользователя и текст комментария и отправлять запрос на сервер для сохранения комментария.


// tutorial15.js
var CommentForm = React.createClass({
  render: function() {
    return (
      <form className="commentForm">
        <input type="text" placeholder="Your name" />
        <input type="text" placeholder="Say something..." />
        <input type="submit" value="Post" />
      </form>
    );
  }
});

Управляемые компоненты

В React компоненты всегда отображают актуальное состояние, а не только в момент инициализации. Поэтому мы будем использовать this.state для сохранения пользовательских данных сразу при вводе. Зададим начальное состояние author и text. В элементах <input> в качестве артибута value будем использовать свойства состояния, а также добавим обработчики onChange. Прочитать подробнее об управляемых компонентах можно в статье Forms.


// tutorial16.js
var CommentForm = React.createClass({
  getInitialState: function() {
    return {author: '', text: ''};
  },
  handleAuthorChange: function(e) {
    this.setState({author: e.target.value});
  },
  handleTextChange: function(e) {
    this.setState({text: e.target.value});
  },
  render: function() {
    return (
      <form className="commentForm">
        <input
          type="text"
          placeholder="Your name"
          value={this.state.author}
          onChange={this.handleAuthorChange}
        />
        <input
          type="text"
          placeholder="Say something..."
          value={this.state.text}
          onChange={this.handleTextChange}
        />
        <input type="submit" value="Post" />
      </form>
    );
  }
});

События

React использует нотацию camelCase для добавления обработчиков событий в компонентах. В примере выше мы добавили обработчики onChange двум элементам <input>. Теперь при вводе текста в <input> будет срабатывать обработчик onChange и изменять состояние компонента. Таким образом, значение в поле ввода будет отражать текущее состояние компонента.

Отправка формы

Давайте сделаем форму интерактивной. Когда пользователь отправить форму, мы должны очистить поля ввода, отправить запрос на сервер и обновить список комментариев. Для начала, добавим обработчик события submit.


// tutorial17.js
var CommentForm = React.createClass({
  getInitialState: function() {
    return {author: '', text: ''};
  },
  handleAuthorChange: function(e) {
    this.setState({author: e.target.value});
  },
  handleTextChange: function(e) {
    this.setState({text: e.target.value});
  },
  handleSubmit: function(e) {
    e.preventDefault();
    var author = this.state.author.trim();
    var text = this.state.text.trim();
    if (!text || !author) {
      return;
    }
    // TODO: send request to the server
    this.setState({author: '', text: ''});
  },
  render: function() {
    return (
      <form className="commentForm" onSubmit={this.handleSubmit}>
        <input
          type="text"
          placeholder="Your name"
          value={this.state.author}
          onChange={this.handleAuthorChange}
        />
        <input
          type="text"
          placeholder="Say something..."
          value={this.state.text}
          onChange={this.handleTextChange}
        />
        <input type="submit" value="Post" />
      </form>
    );
  }
});

Мы добавили обработчик onSubmit, в котором очищаем поля формы. Также мы вызываем метод preventDefault(), чтобы предотвратить отправку формы браузером.

Функции обратного вызова в качестве свойств

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

Нам нужно передать данные от дочернего компонента обратно в родительский. Для этого мы передадим функцию обратного вызова (handleCommentSubmit) в дочерний компонент и свяжем ее с обработчиком события onCommentSubmit. Когда сработает обработчик события, вызовется эта функция:


// tutorial18.js
var CommentBox = React.createClass({
  loadCommentsFromServer: function() {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      cache: false,
      success: function(data) {
        this.setState({data: data});
      }.bind(this),
      error: function(xhr, status, err) {
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    });
  },
  handleCommentSubmit: function(comment) {
    // TODO: submit to the server and refresh the list
  },
  getInitialState: function() {
    return {data: []};
  },
  componentDidMount: function() {
    this.loadCommentsFromServer();
    setInterval(this.loadCommentsFromServer, this.props.pollInterval);
  },
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm onCommentSubmit={this.handleCommentSubmit} />
      </div>
    );
  }
});

Вызовем эту функцию в обработчике отправки формы:


// tutorial19.js
var CommentForm = React.createClass({
  getInitialState: function() {
    return {author: '', text: ''};
  },
  handleAuthorChange: function(e) {
    this.setState({author: e.target.value});
  },
  handleTextChange: function(e) {
    this.setState({text: e.target.value});
  },
  handleSubmit: function(e) {
    e.preventDefault();
    var author = this.state.author.trim();
    var text = this.state.text.trim();
    if (!text || !author) {
      return;
    }
    this.props.onCommentSubmit({author: author, text: text});
    this.setState({author: '', text: ''});
  },
  render: function() {
    return (
      <form className="commentForm" onSubmit={this.handleSubmit}>
        <input
          type="text"
          placeholder="Your name"
          value={this.state.author}
          onChange={this.handleAuthorChange}
        />
        <input
          type="text"
          placeholder="Say something..."
          value={this.state.text}
          onChange={this.handleTextChange}
        />
        <input type="submit" value="Post" />
      </form>
    );
  }
});

Теперь, когда все коллбэки на месте, все, что нам нужно, это отправить запрос на сервер и обновить список:


// tutorial20.js
var CommentBox = React.createClass({
  loadCommentsFromServer: function() {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      cache: false,
      success: function(data) {
        this.setState({data: data});
      }.bind(this),
      error: function(xhr, status, err) {
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    });
  },
  handleCommentSubmit: function(comment) {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      type: 'POST',
      data: comment,
      success: function(data) {
        this.setState({data: data});
      }.bind(this),
      error: function(xhr, status, err) {
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    });
  },
  getInitialState: function() {
    return {data: []};
  },
  componentDidMount: function() {
    this.loadCommentsFromServer();
    setInterval(this.loadCommentsFromServer, this.props.pollInterval);
  },
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm onCommentSubmit={this.handleCommentSubmit} />
      </div>
    );
  }
});

Оптимизация: оптимистичное обновление

Наше приложение полностью готово, но ожидание того, пока ваш комментарий появится в списке, может показаться долгим. Мы можем добавить комментарий заранее, не дожидаясь ответа от сервера.


// tutorial21.js
var CommentBox = React.createClass({
  loadCommentsFromServer: function() {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      cache: false,
      success: function(data) {
        this.setState({data: data});
      }.bind(this),
      error: function(xhr, status, err) {
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    });
  },
  handleCommentSubmit: function(comment) {
    var comments = this.state.data;
    // Optimistically set an id on the new comment. It will be replaced by an
    // id generated by the server. In a production application you would likely
    // not use Date.now() for this and would have a more robust system in place.
    comment.id = Date.now();
    var newComments = comments.concat([comment]);
    this.setState({data: newComments});
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      type: 'POST',
      data: comment,
      success: function(data) {
        this.setState({data: data});
      }.bind(this),
      error: function(xhr, status, err) {
        this.setState({data: comments});
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    });
  },
  getInitialState: function() {
    return {data: []};
  },
  componentDidMount: function() {
    this.loadCommentsFromServer();
    setInterval(this.loadCommentsFromServer, this.props.pollInterval);
  },
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm onCommentSubmit={this.handleCommentSubmit} />
      </div>
    );
  }
});

Поздравляем!

Мы создали блок комментирования всего за несколько шагов. Прочитайте статью why to use React или познакомьтесь с API и вперед! Удачи!