javascript

React.js: понятное руководство для начинающих

  • вторник, 30 октября 2018 г. в 00:19:23
https://habr.com/company/ruvds/blog/428077/
  • Блог компании RUVDS.com
  • JavaScript
  • ReactJS
  • Разработка веб-сайтов


Автор статьи, перевод которой мы публикуем, считает, что, к несчастью, в большинстве из существующих руководств по React не уделяется должного внимания ценным практическим приёмам разработки. Такие руководства не всегда дают тому, кто по ним занимается, понимание того, что такое «правильный подход» к работе с React.

image

В этом руководстве, которое рассчитано на начинающих разработчиков, имеющих знания в области HTML, JavaScript и CSS, будут рассмотрены основы React и самые распространённые ошибки, с которыми может столкнуться программист, пользующийся данной библиотекой.

Почему веб-разработчики выбирают React?


Прежде чем мы приступим к делу, скажем пару слов о том, почему React можно считать наилучшей альтернативой среди средств для разработки веб-интерфейсов. Существует множество UI-фреймворков. Почему стоит выбрать именно React? Для того чтобы ответить на этот вопрос — сравним два самых популярных инструмента для разработки интерфейсов — React и Angular. Надо отметить, что в это сравнение можно было бы включить и набирающий популярность фреймворк Vue.js, но мы ограничимся React и Angular.

▍Декларативный подход к описанию интерфейсов


React-разработка заключается в описании того, что нужно вывести на страницу (а не в составлении инструкций для браузера, посвящённых тому, как это делать). Это, кроме прочего, означает значительное сокращение объёмов шаблонного кода.

В составе Angular, с другой стороны, есть средства командной строки, которые генерируют шаблонный код компонентов. Не кажется ли это немного не тем, чего можно ждать от современных инструментов разработки интерфейсов? Фактически, речь идёт о том, что в Angular так много шаблонного кода, что для того, чтобы его генерировать, даже создано специальное средство.

В React, приступая к разработке, просто начинают писать код. Тут нет шаблонного кода компонентов, который нужно как-то генерировать. Конечно, перед разработкой нужна некоторая подготовка, но, когда дело доходит до компонентов, их можно описывать в виде чистых функций.

▍Чёткий синтаксис


В Angular-коде используются директивы вроде ng-model, ngIf и ngFor. Выглядит такой код довольно-таки громоздко. В React, с другой стороны, применяется синтаксис JSX, который воспринимается как обычный HTML, то есть, для того, чтобы приступить к React-разработке, не нужно изучать принципиально новые вещи. Вот как это выглядит:

const Greetings = ({ firstName }) => (
   <div>Hi, {firstName}</div>
);

▍Правильная кривая обучения


Кривая обучения — это важный фактор, который нужно учитывать при выборе UI-фреймворка. В этой связи надо отметить, что в React имеется меньше абстракций, чем в Angular. Если вы знаете JavaScript, то, вероятно, вы сможете научиться писать React-приложения буквально за день. Конечно, для того, чтобы научиться делать это правильно, потребуется некоторое время, но приступить к работе можно очень и очень быстро.

Если же проанализировать Angular, то окажется, что если вы решите освоить этот фреймворк, вам придётся изучить новый язык (Angular использует TypeScript), а также научиться использовать средства командной строки Angular и привыкнуть к работе с директивами.

▍Особенности механизма привязки данных


В Angular имеется система двусторонней привязки данных. Это, например, выражается в том, что изменения в форме элемента приводят к автоматическому обновлению состояния приложения. Это усложняет отладку и является большим минусом данного фреймворка. При таком подходе, если что-то идёт не так, программист не может совершенно точно знать о том, что именно стало причиной изменения состояния приложения.

В React, с другой стороны, используется односторонняя привязка данных. Это — большой плюс данной библиотеки, так как выражается это в том, что программист всегда точно знает о том, что привело к изменению состояния приложения. Подобный подход к привязке данных значительно упрощает отладку приложений.

▍Функциональный подход к разработке


Я полагаю, что одной из сильнейших сторон React является тот факт, что эта библиотека не принуждает разработчика к использованию классов. В Angular же все компоненты должны быть реализованы в виде классов. Это приводит к чрезмерному усложнению кода, не давая никаких преимуществ.

В React все компоненты пользовательского интерфейса могут быть выражены в виде наборов чистых функций. Использование чистых функций для формирования UI можно сравнить с глотком чистого воздуха.

Теперь, когда мы рассмотрели причины популярности React, которые, вполне возможно, склонят вас в сторону именно этой библиотеки при выборе инструментов для разработки пользовательских интерфейсов, перейдём к практике.

Практика разработки React-приложений


▍Node.js


Node.js — это серверная платформа, поддерживающая выполнение JavaScript-кода, возможности которой пригодятся нам для React-разработки. Если эта платформа ещё у вас не установлена — сейчас самое время это исправить.

▍Подготовка проекта


Здесь мы, для создания основы React-приложения, будем использовать пакет create-react-app от Facebook. Вероятно, это самый популярный подход к настройке рабочего окружения, которое позволяет приступить к разработке. Благодаря create-react-app программист получает в своё распоряжение множество нужных инструментов, что избавляет его от необходимости самостоятельно их подбирать.

Для того чтобы глобально установить create-react-app, воспользуйтесь такой командой:

npm i -g create-react-app

Затем, для создания шаблона приложения, выполните такую команду:

create-react-app react-intro

На этом предварительная подготовка закончена. Для запуска приложения выполните следующие команды:

cd react-intro
npm start

Тут мы переходим в папку проекта и запускаем сервер разработки, который позволяет открыть новое React-приложение, перейдя в браузере по адресу http://localhost:3000/.

▍Структура проекта


Разберёмся теперь с тем, как устроено React-приложение. Для этого откройте только что созданный проект с помощью вашей IDE (я рекомендую Visual Studio Code).

Файл index.html


Находясь в папке проекта, откройте файл, который находится по адресу public/index.html. Вот что вы увидите, сделав это.


Файл index.html

Здесь нас особенно интересует строка <div id="root">. Именно тут будет находиться наше React-приложение. Весь этот элемент будет заменён на код приложения, а всё остальное останется неизменным.

Файл index.js


Теперь откроем файл src/index.js. Именно этот файл выполняет развёртывание React-приложения. И, между прочим, исходный код приложения будет размещаться в директории src.


Файл index.js

Вот строка кода, которая ответственна за вывод того, что мы называем «React-приложением», на страницу:

ReactDOM.render(<App />, document.getElementById('root'));

Эта строка сообщает React о том, что нужно взять компонент App (совсем скоро мы о нём поговорим) и поместить его в div-элемент root, который был определён в только что рассмотренном нами файле index.html.

Разберёмся теперь с конструкцией <App />. Она очень похожа на HTML-код, но это — образец JSX-кода, который представляет собой особый синтаксис JavaScript, используемый React. Обратите внимание на то, что эта конструкция начинается с заглавной буквы A, на то, что это именно <App />, а не <app />. Это так из-за используемого в React соглашения по именованию сущностей. Такой подход позволяет системе различать обычные HTML-теги и компоненты React. Если имена компонентов не будут начинаться с заглавной буквы, React не сможет вывести их на страницу.

Если в некоем .js-файле планируется использовать JSX, там необходимо импортировать React, воспользовавшись следующей командой:

import React from 'react';

Файл App.js


Теперь мы готовы к тому, чтобы взглянуть на код нашего первого компонента. Для этого откроем файл src/App.js.


Файл App.js

Для того чтобы создать компонент React, необходимо сначала создать класс, являющийся наследником React.Component. Именно эту задачу решает строка class App extends Component. Все компоненты React должны содержать реализацию метода render, в котором, как можно догадаться из его названия, выполняется рендеринг компонента, формирование описания его визуального представления. Этот метод должен вернуть HTML-разметку для вывода её на страницу.

Обратите внимание на то, что атрибут className — это эквивалент атрибута class в HTML. Он используется для назначения элементам CSS-классов в целях их стилизации. Ключевое слово class в JavaScript является зарезервированным, его нельзя использовать в качестве имени атрибута.

Повторим то, что мы только что выяснили о компонентах:

  1. Их имена начинаются с заглавной буквы (A в App).
  2. Они расширяют класс React.Component.
  3. Они должны реализовывать метод render, возвращающий разметку.

Теперь поговорим о том, чего, при разработке React-приложений, стоит избегать.

▍Рекомендация №1: не нужно везде использовать классы компонентов


Компоненты в React можно создавать, применяя два подхода. Первый заключается в использовании классов компонентов (Class Component), второй — в использовании функциональных компонентов (Functional Component). Как вы, возможно, заметили, в вышеприведённом примере используются классы. К сожалению, большинство руководств по React для начинающих предлагают использовать именно их.

Что плохого в описании компонентов с использованием механизма классов? Дело в том, что такие компоненты тяжело тестировать и они имеют свойство чрезмерно разрастаться. Эти компоненты подвержены проблеме некачественного разделения ответственности, смешиванию логики и визуального представления (а это усложняет отладку и тестирование приложений). В целом, использование классов компонентов ведёт к тому, что программист, фигурально выражаясь, «стреляет себе в ногу». Поэтому, особенно если речь идёт о начинающих программистах, я порекомендовал бы им совсем не пользоваться классами компонентов.

Итак, мы выяснили, что применение классов для описания компонентов — это не лучшая идея. Какие же у нас есть альтернативы? Ответом на этот вопрос являются функциональные компоненты. Если в компоненте, созданном с использованием класса, нет ничего кроме метода render, то он является отличным претендентом на переработку его в функциональный компонент. Вооружившись этой идеей, подумаем о том, как улучшить компонент App, созданный средством create-react-app:

function App() {
  return (
    <div className="App">
      ...

    </div>
  );
}

export default App;

Видите, что мы тут сделали? А именно, мы убрали класс и заменили метод render конструкцией вида function App() {...}. Если же тут воспользоваться синтаксисом стрелочных функций ES6, то наш код будет выглядеть ещё лучше:

const App = () => (
  <div className="App">
    ...

  </div>
);

export default App;

Мы превратили класс в функцию, возвращающую разметку, которую надо вывести на страницу.

Поразмыслите над этим. В функции, которая возвращает разметку, нет шаблонного кода. Это — практически чистая разметка. Разве это не прекрасно?

Код функциональных компонентов гораздо легче читать, работая с ними, приходится гораздо меньше отвлекаться на стандартные конструкции.

Тут надо отметить, что хотя только что мы сказали о том, что функциональные компоненты предпочтительнее классов компонентов, в этом материале мы будем пользоваться, в основном, классами, так как код классов компонентов оказывается понятнее для новичков, он полагается на меньшее количество абстракций, с его помощью легче демонстрировать ключевые концепции React. Но когда вы в достаточной мере освоитесь в деле разработки React-приложений, настоятельно рекомендуется учитывать, при разработке реальных проектов, то, что было сказано выше. Для того чтобы лучше разобраться с функциональными компонентами — взгляните на этот материал.

▍Знакомство со свойствами


Свойства (props) — это одна из центральных концепций React. Что такое «свойства»? Для того чтобы это понять, вспомните о параметрах, которые передают функциям. В сущности, свойства — это и есть параметры, которые передаются компонентам. Рассмотрим следующий код:

const Greetings = (props) => <div>Hey you! {props.firstName} {props.lastName}!</div>;

const App = () => (
  <div>
    <Greetings firstName="John" lastName="Smith" />
  </div>
);

Тут мы создали компонент Greetings и воспользовались им для того, чтобы поприветствовать человека, которого зовут John Smith, из компонента App. Весь этот код приведёт к формированию следующей HTML-разметки:

<div>
   <div>Hey you! John Smith!</div>
</div>

Фигурные скобки в выражениях вроде {props.name} используются для выделения JavaScript-кода. Компоненту Greetings передаются, в виде параметров, свойства firstName и lastName. Мы работаем с ними, обращаясь к свойствам объекта props.

Обратите внимание на то, что компоненту передаётся единственный объект props, а не два значения, представляющие свойства firstName и lastName.

Код можно упростить, воспользовавшись возможностями ES6 по деструктурированию объектов:

const Greetings = ({ firstName, lastName }) => <div>Hey you! {firstName} {lastName}!</div>;

Как видите, тут конструкция (props) была заменена на ({ firstName, lastName }). Этой заменой мы сообщаем системе о том, что нас интересуют лишь два свойства объекта props. И это, в свою очередь, позволяет нам напрямую обращаться к значениям firstName и lastName, не указывая в явном виде свойства объекта наподобие props.firstName.

Что если для решения такой же задачи мы, вместо функциональных компонентов, использовали бы компоненты, основанные на классах? В таком случае код компонента Greetings выглядел бы так:

class Greetings extends React.Component {
  render() {
    return (
      <div>Hey you! {this.props.firstName} {this.props.lastName}!</div>
    );
  }
}

Не знаю, какие ощущения подобный код вызывает у вас, но мне он кажется перегруженным вспомогательными механизмами. Здесь, в частности, для доступа к свойствам, необходимо пользоваться конструкциями вида this.props.

▍Принцип единственной ответственности


Принцип единственной ответственности (Single Responsibility Principle, SRP) — это один из важнейших принципов программирования, которого следует придерживаться. Он говорит нам о том, что модуль должен решать только одну задачу и должен делать это качественно. Если разрабатывать проект, не следуя только одному этому принципу, код такого проекта может превратиться в кошмарную конструкцию, которую невозможно поддерживать.

Как можно нарушить принцип единственной ответственности? Чаще всего это происходит тогда, когда несвязанные друг с другом механизмы размещают в одних и тех же файлах. В этом материале мы часто будем обращаться к данному принципу.

Новички обычно размещают в одном файле множество компонентов. Например, у нас код компонентов Greetings и App находится в одном файле. На практике так поступать не следует, так как это нарушает SRP.

Даже очень маленькие компоненты (вроде нашего компонента Greetings) нужно размещать в отдельных файлах.

Поместим код компонента Greetings в отдельный файл:

import React from "react";

const Greetings = ({ firstName, lastName }) => (
    <div>
        Hey you! {firstName} {lastName}!
    </div>
);

export default Greetings;

Затем воспользуемся этим компонентом в компоненте App:

import Greetings from "./Greetings";
const App = () => (
  ...

);

Обратите внимание на то, что имя файла должно совпадать с именем компонента. То есть, код компонента App должен размещаться в файле App.js, код компонента Greetings — в файле Greetings.js, и так далее.

▍Знакомство с состоянием приложения


Состояние (state) — это ещё одна из центральных концепций React. Именно здесь хранятся данные приложения — то есть то, что может меняться. Нужно сохранить что-то, введённое в поле формы? Используйте состояние. Нужно сохранить очки, набранные игроком в браузерной игре? Для этого тоже надо использовать состояние приложения.

Создадим простую форму, с помощью которой пользователь может ввести своё имя. Обратите внимание на то, что здесь я намеренно использую для описания компонента класс, так как это облегчает демонстрацию рассматриваемой концепции. О том, как преобразовать компонент, созданный с использованием класса, в функциональный компонент, можете почитать здесь.

import React from "react";

class SimpleForm extends React.Component {
  render() {
    return (
      <div>
        <input type="text" name="firstName" />
        <Greetings firstName="John" />
      </div>
    );
  }
}

const App = () => (
  <div>
    <SimpleForm />
  </div>
);

В поле формы пользователь может что-то ввести, и это хорошо. Однако если вы внимательно прочли этот код, то вы можете заметить, что в качестве имени пользователя, в выводимом на странице приветствии, всегда используется John. Что если так зовут далеко не всех наших пользователей? Если это так, то мы ставим себя в не очень-то удобное положение.

Как использовать значение, введённое в поле? В React не полагается обращаться к элементам DOM напрямую. Решить эту задачу нам помогут обработчики событий и состояние приложения.

class SimpleForm extends React.Component {
  state = {
    firstName: "",
  };

  onFirstNameChange = event =>
    this.setState({
      firstName: event.target.value
    });

  render() {
    return (
      <div>
        <input type="text" name="firstName" onChange={this.onFirstNameChange} />

        <Greetings firstName={this.state.firstName} />
      </div>
    );
  }
}

Состояние можно представить себе как простой JavaScript-объект, который, в виде свойства, хранится в классе компонента SimpleForm. В этот объект мы добавляем свойство firstName.

Тут мы оснастили поле firstName обработчиком события. Он запускается каждый раз когда пользователь вводит хотя бы один символ в поле. В классе за обработку события onChange отвечает свойство onFirstNameChange, в представляющей которое функции выполняется команда следующего вида:

this.setState(...)

Именно здесь выполняется обновление состояния компонента. Состояние компонентов не обновляют напрямую. Это делается только с использованием метода setState. Для того чтобы записать новое значение в свойство firstName, мы просто передаём этому методу объект, содержащий то, что нужно записать в состояние:

{ firstName: event.target.value }

В данном случае event.target.value — это то, что пользователь ввёл в поле формы, а именно — его имя.

Обратите внимание на то, что мы не объявили onFirstNameChange в виде метода. Очень важно, чтобы подобные вещи объявлялись бы в виде свойств класса, содержащих стрелочные функции, а не в виде методов. Если объявить подобную функцию в виде метода, тогда this будет привязано к элементу формы, который вызывает этот метод, а не к классу, как мы могли бы ожидать. Эта мелочь часто сбивает с толку новичков. Это — одна из причин рекомендации по использованию функциональных компонентов, а не классов компонентов.

▍Проверка данных, введённых в форму


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

Добавим в компонент обработчик события onBlur, который вызывается в тот момент, когда пользователь покидает поле ввода. В состояние приложения добавим ещё одно свойство — firstNameError. Если при вводе имени возникла ошибка, будем выводить сообщение об этом под полем.

Разберём этот код.

class SimpleForm extends React.Component {
  state = {
    firstName: "",
    firstNameError: "",
  };

  validateName = name => {
    const regex = /[A-Za-z]{3,}/;

    return !regex.test(name)
      ? "The name must contain at least three letters. Numbers and special characters are not allowed."
      : "";
  };

  onFirstNameBlur = () => {
    const { firstName } = this.state;

    const firstNameError = this.validateName( firstName );

    return this.setState({ firstNameError });
  };

  onFirstNameChange = event =>
    this.setState({
      firstName: event.target.value
    });

  render() {
    const { firstNameError, firstName } = this.state;

    return (
      <div>
        <div>
          <label>
            First name:
            <input
              type="text"
              name="firstName"
              onChange={this.onFirstNameChange}
              onBlur={this.onFirstNameBlur}
            />
            {firstNameError && <div>{firstNameError}</div>}
          </label>
        </div>

        <Greetings
          firstName={firstName}
        />
      </div>
    );
  }
}

Состояние приложения


Разрабатывая систему проверки ввода, мы, для начала, добавили в состояние новое свойство: firstNameError:

state = {
   ...

   firstNameError: "",
};

Функция проверки данных


Проверка данных выполняется в стрелочной функции validateName. Она проверяет введённое имя с помощью регулярного выражения:

validateName = name => {
  const regex = /[A-Za-z]{3,}/;
  return !regex.test(name)
     ? "The name must contain at least three letters..."
     : "";
}

Если проверка не удалась — мы возвращаем из функции сообщение об ошибке. Если имя прошло проверку — возвращаем пустую строку, которая указывает на то, что ошибок при проверке имени не найдено. Тут мы, ради краткости кода, пользуемся тернарным оператором JavaScript.

Обработчик события onBlur


Взглянем на обработчик события onBlur, который вызывается когда пользователь покидает поле ввода:

onFirstNameBlur = () => {
  const { firstName } = this.state;
  const firstNameError = this.validateName( firstName );
  return this.setState({ firstNameError });
};

Здесь мы извлекаем из состояния свойство firstName, пользуясь возможностями ES6 по деструктурированию объектов. Первая строка этого кода эквивалентна такой:

const firstName = this.state.firstName;

Затем мы вызываем вышеописанную функцию проверки данных, передавая ей firstName, и записываем в свойство состояния firstNameError то, что возвратит эта функция. Если проверка не удалась, в это свойство попадёт сообщение об ошибке. Если удалась — туда будет записана пустая строка.

Метод render


Рассмотрим метод компонента render():

render() {
   const { firstNameError, firstName} = this.state;
   ...

}

Тут мы снова пользуемся деструктурирующим присваиванием для извлечения данных из состояния.

<input
  ...

  onBlur={this.onFirstNameBlur}
/>

Эта строка назначает функцию onFirstNameBlur обработчиком события onBlur.

{firstNameError && <div>{firstNameError}</div>}

Здесь мы пользуемся особенностями вычисления логических выражений в JavaScript. Элемент div, содержащий сообщение об ошибке, будет выведен лишь в том случае, если значение firstNameError может быть приведено к true.

▍Стилизация


Если вы воспроизводили у себя то, о чём мы тут говорили, то вы могли заметить, что наша форма выглядит не слишком симпатично. Давайте это исправим, воспользовавшись встроенными стилями:

render() {
  const { firstNameError, firstName } = this.state;

  return (
    <div
        style={{
          margin: 50,
          padding: 10,
          width: 300,
          border: "1px solid black",
          backgroundColor: "black",
          color: "white"
        }}
      >
      <div style={{marginBottom: 10}}>
        <label>
          First name:
          <input
            style={{backgroundColor: '#EFEFFF', marginLeft: 10}}
            type="text"
            name="firstName"
            onChange={this.onFirstNameChange}
            onBlur={this.onFirstNameBlur}
          />
          {firstNameError && <div style={{color: 'red', margin: 5}}>{firstNameError}</div>}
        </label>
      </div>

      <Greetings
        firstName={firstName}
      />
    </div>
  );
}

Стилизация компонентов в React выполняется путём передачи стилей в атрибут style.

Признаю, что я не дизайнер, но то, что у меня получилось, выглядит сейчас гораздо лучше, чем прежде. Вот какой стала теперь форма, в которой выводится сообщение об ошибке.


Стилизованная форма с сообщением об ошибке

▍Рекомендация №2: избегайте использования стилей внутри компонентов


Тут мы снова сталкиваемся с примером того, чего лучше не делать, разрабатывая React-приложения. Проблема, о которой идёт речь, то есть — размещение стилей в методе render, к сожалению, распространена чрезвычайно сильно. Почему это плохо? Дело в том, что это нарушает принцип единственной ответственности. Кроме того, стили загромождают код компонентов, что значительно ухудшает читабельность программы.

Как этого избежать? На самом деле, довольно просто. Достаточно создать специальный объект style, который и будет содержать стили. Рекомендуется размещать стили в отдельном файле. Например, вот код подобного файла с именем style.js:

// style.js:
const style = {
    form: {
        margin: 50,
        padding: 10,
        width: 300,
        border: "1px solid black",
        backgroundColor: "black",
        color: "white"
    },
    inputGroup: {
        marginBottom: 10
    },
    input: {
        backgroundColor: "#EFEFFF",
        marginLeft: 10
    },
    error: {
        color: "red",
        margin: 5
    }
};

export default style;

После того, как этот файл создан, подключим его в компоненте SimpleComponent:

import style from './style';

class SimpleForm extends React.Component {
    ...

    
      render() {
        const { firstNameError, firstName } = this.state;
    
        return (
          <div style={style.form}>
            <div style={style.inputGroup}>
              <label>
                First name:
                <input
                  style={style.input}
                  type="text"
                  name="firstName"
                  onChange={this.onFirstNameChange}
                  onBlur={this.onFirstNameBlur}
                />
                {firstNameError && (
                  <div style={style.error}>{firstNameError}</div>
                )}
              </label>
            </div>
    
            <Greetings firstName={firstName} />
          </div>
        );
      }
  }
  export default SimpleForm;

Этот код выглядит куда чище, чем его предыдущий вариант. Поэтому примите себе за правило размещать стили в отдельных файлах.

▍Добавление полей формы


Сделаем нашу форму немного интереснее, добавив в неё поле для ввода фамилии пользователя:

class SimpleForm extends React.Component {
  state = {
    ...

    lastName: "",
    lastNameError: ""
  };

  validateName = ...;

  onFirstNameBlur = ...;

  onFirstNameChange = ...;

  onLastNameBlur = () => {
    const { lastName } = this.state;

    const lastNameError = this.validateName(lastName);

    return this.setState({ lastNameError });
  };

  onLastNameChange = event =>
    this.setState({
      lastName: event.target.value
    });

  render() {
    const { firstNameError, firstName, lastName, lastNameError } = this.state;

    return (
      <div style={style.form}>
        <div style={style.inputGroup}>
          <label>
            First name:
            <input
              style={style.input}
              type="text"
              name="firstName"
              onChange={this.onFirstNameChange}
              onBlur={this.onFirstNameBlur}
            />
            {firstNameError && <div style={style.error}>{firstNameError}</div>}
          </label>
        </div>

        <div style={style.inputGroup}>
          <label>
            Last name:
            <input
              style={style.input}
              type="text"
              name="lastName"
              onChange={this.onLastNameChange}
              onBlur={this.onLastNameBlur}
            />
            {lastNameError && <div style={style.error}>{lastNameError}</div>}
          </label>
        </div>

        <Greetings firstName={firstName} lastName={lastName} />
      </div>
    );
  }
}

export default SimpleForm;

В обновлённом компоненте не так уж и много изменений — мы просто скопировали код, используемый для описания firstName и создали копии обработчиков событий.

Неужто это я написал слово «скопировали»? Копирование кода — это то, чего стоит всеми силами избегать.

▍Рекомендация №3: разделяйте код на небольшие фрагменты


Монолитный код, не разделённый на части, это проблема, которая, как и многие другие, нарушает принцип единственной ответственности. Хорошо написанный код должен читаться как поэма, а я готов поспорить, что код метода render нашего компонента выглядит куда хуже. Займёмся решением этой проблемы.

Поля ввода, используемые в форме, практически идентичны, и тому и другому нужны некие механизмы проверки данных. Применим к нашему компоненту некоторые приёмы рефакторинга и создадим компонент TextField, подходящий для повторного использования.

import React from 'react'
import style from "./style";

const TextField = ({name, onChange, onBlur, error, label}) => (
  <div style={style.inputGroup}>
    <label>
      {label}
      <input
        style={style.input}
        type="text"
        name={name}
        onChange={onChange}
        onBlur={onBlur}
      />
      {error && <div style={style.error}>{error}</div>}
    </label>
  </div>
);

export default TextField;

Я, создавая этот компонент, просто извлёк код одного из полей ввода из метода render и оформил его в виде функционального компонента. То, что в разных экземплярах подобного компонента может меняться, передано ему в виде свойств.

Вот как можно использовать этот новый компонент в компоненте SimpleForm:

...

import TextField from './TextField';

class SimpleForm extends React.Component {
  ...

  render() {
    const { firstNameError, firstName, lastName, lastNameError } = this.state;

    return (
      <div style={style.form}>
        <TextField name="firstName"
                   label="First name:"
                   onChange={this.onFirstNameChange}
                   onBlur={this.onFirstNameBlur}
                   error={firstNameError} />

        <TextField name="lastName"
                   label="Last name:"
                   onChange={this.onLastNameChange}
                   onBlur={this.onLastNameBlur}
                   error={lastNameError} />

        <Greetings firstName={firstName} lastName={lastName} />
      </div>
    );
  }
}

Такой код читать легче, чем прежний. Мы можем пойти ещё дальше, создав отдельные компоненты TextField для имени и фамилии. Вот код компонента FirstNameField:

import React from 'react';
import TextField from './TextField';

const FirstNameField = ({...rest}) => (
  <TextField name="firstName"
              label="First name:"
              {...rest} />
);

export default FirstNameField;

Тут мы просто возвращаем компонент, заранее подготовленный для вывода имени пользователя. В конструкции ({...rest}) используется синтаксис оставшихся параметров (оператор rest, выглядящий как три точки). Благодаря использованию этого оператора оказывается, что всё, что будет передано компоненту в виде свойств, попадёт в объект rest. Затем, для того, чтобы передать свойства компоненту TextField, используется так называемый «оператор расширения», или оператор spread (он, в конструкции {...rest}, тоже выглядит как три точки). Тут берётся объект rest, из него выделяются его свойства, которые передаются компоненту TextField.

Другими словами, все эти конструкции позволяют выразить следующую идею: мы хотим взять всё, что пришло в компонент FirstNameField и передать это в неизменном виде компоненту TextField.

Похожим образом устроен и компонент LastNameField:
Вот каким теперь будет код формы:

...

import FirstNameField from './FirstNameField';
import LastNameField from './LastNameField';

class SimpleForm extends React.Component { 
  ...


  render() {
    const { firstNameError, firstName, lastName, lastNameError } = this.state;

    return (
      <div style={style.form}>
        <FirstNameField onChange={this.onFirstNameChange}
                        onBlur={this.onFirstNameBlur}
                        error={firstNameError} />

        <LastNameField onChange={this.onLastNameChange}
                        onBlur={this.onLastNameBlur}
                        error={lastNameError} />

        <Greetings firstName={firstName} lastName={lastName} />
      </div>
    );
  }
}

Теперь код компонента выглядит гораздо лучше.

▍О классах компонентов


Перечислим причины, по которым не рекомендуется использовать компоненты, основанные на классах, отдавая предпочтение функциональным компонентам:

  • Компоненты, основанные на классах, в отличие от функциональных компонентов, сложнее тестировать.
  • Классы компонентов плохо поддерживают концепцию разделения ответственности. Если разработчик не уделяет качеству кода должного внимания, он будет складывать весь код в один класс, который, со временем, может вырасти в монстра размером в 1000 строк (мне не раз приходилось видеть подобные классы).
  • Использование классов компонентов подталкивает разработчика к тому, чтобы размещать в одном и том же месте логику приложения и описание его интерфейса. А это, опять же, плохо с точки зрения концепции разделения ответственности.
  • Компоненты, созданные с использованием классов, нельзя назвать сущностями, напоминающими чистые функции. Это усложняет прогнозирования поведения подобных компонентов. Функциональные компоненты, с другой стороны, являются чистыми функциями. Это выражается в том, что они всегда, при передаче им одних и тех же входных данных, выдают одну и ту же разметку.
  • Применение функциональных компонентов способствуют тому, чтобы разработчик размышлял об архитектуре приложений, что, в свою очередь, улучшает её.
  • При использовании функциональных компонентов нет необходимости пользоваться ключевым словом this, которое всегда являлось источником путаницы.

Итоги


Хотя этот материал и получился достаточно длинным, о создании React-приложений можно говорить ещё очень долго. В частности, о том, как правильно организовывать код, и о том, какие приёмы разработки рекомендуется и не рекомендуется использовать. Несмотря на то, что это руководство не является всеобъемлющим, мы полагаем, что если благодаря ему состоялось ваше первое знакомство с React, то вы, пользуясь тем, что сегодня узнали, сможете эффективно осваивать эту UI-библиотеку.

→ Исходный код примеров, рассмотренных в этом материале, можно найти здесь

Уважаемые читатели! Если сегодня, в ходе прочтения этой статьи, вы написали свои первые строки React-кода, просим поделиться впечатлениями.