Написать пост

9 полезных советов для тех, кто начинает знакомство с React.js

Аватар Иван Бирюков

Обложка поста 9 полезных советов для тех, кто начинает знакомство с React.js

Рассказывает Кэм Джексон 

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

Я рассчитываю, что вы разбираетесь в базовых понятиях; если слова “component”, “props” и “state” вам не знакомы, то сперва стоит ознакомиться с официальными страничками Getting started и Tutorial. Кроме того, я буду использовать JSX, поскольку это более удобный синтаксис для написания компонентов.

1. Это просто библиотека для работы с представлениями

Сперва разберем основы. React — это не очередной MVC-фреймворк или какой-то другой фреймворк. Это просто библиотека для рендеринга ваших представлений. Если вы пришли из мира MVC, то стоит понять, что React — это только “V”, а “M” и “C” придётся поискать где-то ещё.

2. Компоненты должны быть небольшими

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

			const LatestPostsComponent = props => (
  <section>
    <div><h1>Latest posts</h1></div>
    <div>
      { props.posts.map(post => <PostPreview key={post.slug} post={post}/>) }
    </div>
  </section>
);
		

Сам компонент — это <section>, с двумя <div>‘ами внутри. В первом находится заголовок, а второй выводит какие-то данные, отображая <PostPreview> каждого элемента. Примерно такими должны быть ваши компоненты.

3. Пишите функциональные компоненты

Раньше было всего два способа определения React-компонентов, первый — это React.createClass():

			const MyComponent = React.createClass({
  render: function() {
    return <div className={this.props.className}/>;
  }
});
		

… а второй — классы ES6:

			class MyComponent extends React.Component {
  render() {
    return <div className={this.props.className}/>;
  }
}
		

React 0.14 принёс новый синтаксис определения компонентов как функций от свойств:

			const MyComponent = props => (
  <div className={props.className}/>
);
		

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

			class LatestPostsComponent extends React.Component {
  render() {
    const postPreviews = renderPostPreviews();

    return (
      <section>
        <div><h1>Latest posts</h1></div>
        <div>
          { postPreviews }
        </div>
      </section>
    );
  }

  renderPostPreviews() {
    return this.props.posts.map(post => this.renderPostPreview(post));
  }

  renderPostPreview(post) {
    return (
      <article>
        <h3><a href={`/post/${post.slug}`}>{post.title}</a></h3>
        <time pubdate><em>{post.posted}</em></time>
        <div>
          <span>{post.blurb}</span>
          <a href={`/post/${post.slug}`}>Read more...</a>
        </div>
      </article>
    );
  }
}
		

Этот класс не так уж и плох. Мы уже вынесли пару методов из метода отрисовки и неплохо инкапсулировали саму идею рендеринга последних постов. Перепишем этот код, используя функциональный синтаксис:

			const LatestPostsComponent = props => {
  const postPreviews = renderPostPreviews(props.posts);

  return (
    <section>
      <div><h1>Latest posts</h1></div>
      <div>
        { postPreviews }
      </div>
    </section>
  );
};

const renderPostPreviews = posts => (
  posts.map(post => this.renderPostPreview(post))
);

const renderPostPreview = post => (
  <article>
    <h3><a href={`/post/${post.slug}`}>{post.title}</a></h3>
    <time pubdate><em>{post.posted}</em></time>
    <div>
      <span>{post.blurb}</span>
      <a href={`/post/${post.slug}`}>Read more...</a>
    </div>
  </article>
);
		

Код почти не изменился, правда, теперь у нас есть чистые функции, а не методы класса. Однако, это весьма разные вещи. В первом примере я вижу class LatestPostsComponent { и сразу просматриваю код в поисках закрывающей скобки, думая: “Это конец класса, а значит, и конец компонента.” Для сравнения, во втором примере я вижу const LatestPostsComponent = props => { и ищу лишь конец этой функции, думая: “Это конец функции, а значит, и конец модуля. Стоп, но что это за код после моего компонента, в том же модуле? А, это другая функция, которая принимает данные и отрисовывает представление! Я могу вынести её в отдельный компонент!”

В будущем React будет оптимизирован так, чтобы функциональные компоненты были более эффективными, но пока что их производительность находится под большим вопросом; я рекомендую вам ознакомиться с этим материалом для прояснения картины.

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

Второе ограничение — к функциональным компонентам нельзя прикрепить состояние, и это тоже является преиммуществом, поскольку я советую: …

4. Пишите компоненты без состояний

Стоит сказать, что больше всего боли при написании React-приложений я ощутил от компонент с обширным использованием состояния.

Состояния затрудняют тестирование

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

Состояния затрудняют понимание компонентов

Читая код компонента, насыщенного состояниями, возникает очень много вопросов: “Это состояние уже было инициализировано?”, “Что произойдёт, если я изменю это состояние здесь?”, “Где ещё изменяется это состояние?”, “Есть ли здесь состояние гонки (race condition)?” и подобных — а это лишняя головная боль.

Состояния слишком упрощают добавление бизнес-логики в компоненты

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

Состояния затрудняют обмен информацией между частями приложения

Когда компонент обладает состоянием, его можно передать ниже по иерархии компонентов, но не в других направлениях.

Конечно, иногда в том, что у конкретного компонента есть полный доступ к конкретному состоянию, есть смысл, и тогда можно использовать this.setState — это вполне законная часть API React-компонентов. Например, если пользователь вводит информацию в поле, нет смысла делать каждое нажатие клавиш доступным всему приложению, поэтому достаточно будет отслеживать состояние поля самому полю, а по окончании ввода передать дальше введённое значение.

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

5. Используйте Redux.js

В первом пункте я сказал, что React нужен лишь для работы с представлениями. Но куда же поместить все состояния и логику?

Вы наверняка слышали про Flux — SPA (style/pattern/architecture) для разработки веб-приложений, зачастую используемых с React. Существуют несколько фреймворков, реализующих идеи Flux, но я однозначно рекомендую Redux.js.

Вот краткое описание принципов работы Redux:

  1. Компоненты принимают коллбэки как свойства и вызывают их, когда происходит UI-событие;
  2. Эти коллбэки создают и отправляют действия в зависимости от события;
  3. Редюсеры обрабатывают действия, вычисляя новое состояние;
  4. Новое состояние всего приложения помещается в одно хранилище;
  5. Компоненты получают новое состояние как свойство и переотрисовывают себя при необходимости.

Большая часть вышеобозначенных признаков встречаются не только в Redux, но он предоставляет очень простую их реализацию и крошечный API. Перенеся немаленький проект с Alt.js на Redux, я выделил несколько преимуществ:

  • Редюсеры — это чистые функции, которые просто делают следующее: oldState + action = newState. Каждый редюсер вычисляет отдельную часть состояния, которые затем объединяются. Это заметно упрощает тестирование бизнес-логики и состояний.
  • API меньше, проще и лучше задокументирован.
  • Если вы используете Redux как положено, полагаться на него будет совсем небольшое количество компонентов; остальные будут лишь получать состояния и коллбэки как свойства.

Есть ещё несколько библиотек, которые прекрасно дополняют Redux:

  • Immutable.js — неизменяемые структуры данных в JavaScript! В них стоит хранить состояния, чтобы они случайно не изменились.
  • redux-thunk — она используется, когда помимо изменения состояния должен возникнуть ещё какой-то “побочный эффект”.
  • reselect — используйте её для создания компонуемых представлений.

6. Всегда используйте propTypes

propTypes предоставляют нам очень простой способ повышения безопасности наших компонентов. Они выглядят так:

			const ListOfNumbers = props => (
  <ol className={props.className}>
    {
      props.numbers.map(number => (
        <li>{number}</li>)
      )
    }
  </ol>
);

ListOfNumbers.propTypes = {
  className: React.PropTypes.string.isRequired,
  numbers: React.PropTypes.arrayOf(React.PropTypes.number)
};
		

В процессе разработки, если какому-то компоненту не будет передано нужное свойство или свойство иного типа, React создаст лог ошибки. Преимущества:

  • Можно заранее ловить баги;
  • Если вы используете isRequired, вам не нужно проверять на undefined или null;
  • Решается вопрос документирования.

Хотя это и выглядит как агитация за статическую типизацию, это не так. Я предпочитаю динамические типы за их простоту, но propTypes дополнительно повышают безопасность компонентов, поэтому я не вижу причин их не использовать.

Также стоит настроить тесты на провал при столкновении с ошибками propType. Это просто и работает:

			beforeAll(() => {
  console.error = error => {
    throw new Error(error);
  };
});
		

7. Используйте неглубокий рендеринг

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

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

Чаще всего я пишу три типа юнит-тестов компонентов:

Логика отрисовки

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

			const Image = props => {
  if (props.loading) {
    return <LoadingIcon/>;
  }

  return <img src={props.src}/>;
};
		

Мы можем протестировать его так:

			describe('Image', () => {
  it('renders a loading icon when the image is loading', () => {
    const image = shallowRender(<Image loading={true}/>);

    expect(image.type).toEqual(LoadingIcon);
  });

  it('renders the image once it has loaded', () => {
    const image = shallowRender(<Image loading={false} src="https://example.com/image.jpg"/>);

    expect(image.type).toEqual('img');
  });
});
		

Элементарно! Стоит отметить, что API для неглубокого рендеринга более сложен, чем в моём примере. Функция shallowRender написана нами как обёртка API для его упрощения.

Возвращаясь к компоненту ListOfNumbers, вот как можно протестировать его корректность:

			describe('ListOfNumbers', () => {
  it('renders an item for each provided number', () => {
    const listOfNumbers = shallowRender(<ListOfNumbers className="red" numbers={[3, 4, 5, 6]}/>);

    expect(listOfNumbers.props.children.length).toEqual(4);
  });
});
		

Преобразование свойств

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

			const TextWithArrayOfClassNames = props => (
  <div>
    <p className={props.classNames.join(' ')}>
     {props.text}
    </p>
  </div>
);

describe('TextWithArrayOfClassNames', () => {
  it('turns the array into a space-separated string', () => {
    const text = 'Hello, world!';
    const classNames = ['red', 'bold', 'float-right'];
    const textWithArrayOfClassNames = shallowRender(<TextWithArrayOfClassNames text={text} classNames={classNames}/>);

    const childClassNames = textWithArrayOfClassNames.props.children.props.className;
    expect(childClassNames).toEqual('red bold float-right');
  });
});
		

Часто такой подход критикуют за props.children.props.children… Хотя такой код и не очень красив, он полезен тем, что даёт понять: если в одном тесте слишком много props.children, компонент стоит уменьшить.

Ещё одним замечанием является то, что такие тесты очень зависимы от внешней реализации компонента в том плане, что даже малейшее изменение структуры DOM ломает все тесты. С этим я согласен, но бороться с этим можно, — да-да — уменьшая размер компонентов, что снизит количество потенциально неустойчивых тестов.

Взаимодействие с пользователем

Конечно, компоненты ещё и интерактивны:

			const RedInput = props => (
  <input className="red" onChange={props.onChange} />
)
		

Вот мой любимый способ тестирования:

			describe('RedInput', () => {
  it('passes the event to the given callback when the value changes', () => {
    const callback = jasmine.createSpy();
    const redInput = shallowRender(<RedInput onChange={callback}/>);

    redInput.props.onChange('an event!');
    expect(callback).toHaveBeenCalledWith('an event!');
  });
});
		

Пример тривиален, но я думаю, что вы уловили суть.

Интеграционное тестирование

Пока что мы рассмотрели лишь юнит-тестирование, но нужны какие-то тесты более высокого уровня для тестирования всего приложения целиком. Не буду вдаваться в детали:

  1. Отрисовывайте всё дерево компонентов (вместо неглубокого рендеринга).
  2. Углубитесь в DOM (используя React TestUtilsjQuery и т.д.) для поиска самых важных элементов, и:подтвердите их HTML-атрибуты или содержимое, илисимулируйте DOM-события и подтвердите побочные эффекты (изменения DOM или путей, AJAX-вызовы и т.д.).

8. Используйте JSX, ES6, Babel, Webpack и NPM

Единственной вещью, специфичной для react, является JSX. Его единственным недостатком является небольшое увеличение времени сборки, но это решается при помощи Babel.

Если уж мы начали использовать Babel, нет причин отказываться от использования всех функций ES6. Чувствуется, что JavaScript начинает расти как язык, учитывая, сколько времени требуется для подготовки всех инструментов.

Закончим мы использованием Webpack для бандлинга нашего кода и NPM для управления пакетами.

9. Используйте инструменты разработчиков React и Redux

Говоря об инструментах, React и Redux в этом плане очень хороши. Инструменты для React позволяют проверить отрисованное дерево элементов React, что очень удобно. Инструменты для Redux впечатляют ещё больше. Добавить их можно в виде зависимости или расширения браузера.

JavaScript
Для начинающих
Советы
React
110929