9 принципов, которые должен знать новичок в React.js

Автор статьи: Cam Jackson. Оригинал статьи здесь. От переводчика: статья не лишена спорных или субъективных моментов, но, все же, большинство изложенных принципов достаточно полезны.

Я использую React.js с 2015 года. По большому счету, это не такой уж и большой срок, но в постоянно меняющемся мире JavaScript, этого достаточно, чтобы мнить себя умудренным старцем. Недавно я помог нескольким людям советами по началу работы с React, поэтому я подумал, что было бы неплохо опубликовать некоторые из них здесь, чтобы больше людей их увидели. Это все — принципы, которые я хотел бы знать, когда я начинал изучать React, или принципы, которые действительно помогли мне разобраться в React’e.

Я предполагаю, что вы уже знаете самые основы. Но если слова component, props, или state вам не знакомы, то вам лучше бы сначала прочитать официальные Hello World или Руководство. Также, я буду использовать JSX, потому что он предоставляет гораздо более краткий и выразительный синтаксис для написания компонентов.

1. Это просто библиотека отображения

Давайте сначала разберемся с основами. React это не очередной MVC-фреймворк или какой-либо другой фреймворк. Это всего лишь библиотека для рендеринга ваших отображений (views). Если вы пришли из мира MVC, то вам нужно осознать, что React это буква ‘V’ в этой аббревиатуре, и вы должны поискать где-нибудь в другом месте, когда дело доходит до ‘M’ и ‘C’, иначе в результате вы получите кучу дурно пахнущего React-кода. Подробнее об этом далее.

2. Делайте небольшие компоненты

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

Компонент представляет собой <section> с двумя <div> внутри него. Первый содержит заголовок, второй — обходит массив с данными, выводя <PostPreview> для каждого элемента массива. Эта последняя часть, выдающая <PostPreview> как отдельный компонент — очень важна. Я считаю, что это и есть подходящий размер для компонента.

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

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

Затем появился способ, использующий классы ES6:

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

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

Этот класс не такой плохой, как вообще могут быть классы. Мы уже выделили пару методов из метода render(), разделив его на небольшие блоки с понятными названиями. Задача вывода кратких версий последних записей блога решена. Теперь давайте перепишем тот же самый код, используя функциональный синтаксис:

Большая часть кода не изменилась, только теперь у нас голые функции вместо методов класса. Тем не менее, для меня это большая разница. Когда компонент разбит на функции, мне субъективно проще поймать тот момент, когда этот компонент уже пора разбить на более мелкие компоненты. Таким образом, “функциональный” синтаксис помогает мне следовать пункту №2.

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

Важно отметить, что функциональные компоненты имеют несколько “ограничений”, которые я бы, скорее, назвал их сильными сторонами. Во-первых, функциональному компоненту нельзя присвоить атрибут ref (подробнее о нем здесь). Хотя он и является удобным способом для взаимодействия с дочерними компонентами из родительского, но, по моим ощущениям, это Неправильный Способ Разработки на React. ref -атрибуты поощряют очень императивный и почти jquery-подобный подход к написанию компонентов, уводя нас в сторону от философии функциональности и однонаправленности потока данных, ради которой мы и выбираем React.

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

4. Создавайте компоненты, не имеющие состояние (stateless)

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

Наличие состояния затрудняет тестирование компонентов

Нет ничего проще для тестирования, чем чистые функции. Так зачем же нам лишать себя возможности делать компоненты в виде чистых функций, добавляя к ним состояние? Во время тестирования компонентов с состоянием, нам приходится приводить компоненты к определенному состоянию, чтобы затем протестировать их поведение. Нам, также, приходится возиться со всем разнообразием комбинаций состояния (которое компонент может изменить в любой момент) и входных атрибутов (props, которые компонент вообще не контролирует), и выяснять которые из них тестировать и как. А когда компоненты выполнены в виде простых функций от входных данных — тестировать намного легче. (Подробнее о тестировании ниже).

Наличие состояния затрудняет понимание работы компонента

Когда читаешь код компонента со сложным и развесистым состоянием, то приходится сильнее напрягать голову, чтобы уследить за всем что происходит. Постоянно возникают вопросы из разряда: “Это состояние уже инициализировано?”, “Что будет, если я поменяю состояние в этом месте?”, “Возникает ли здесь race condition?”. Просмотр компонентов, имеющих состояние, с целью понять их поведение, становится очень болезненным.

Наличие состояния слишком легко позволяет вставить в компонент бизнес-логику

На самом деле, просмотр компонентов с целью выявления логики поведения это не то, что мы должны делать. Вспомните, React это библиотека отображения, таким образом логика рендеринга в компоненте это ОК, но бизнес-логика в компоненте — это очень плохо. И когда большая часть состояния приложения уже находится прямо здесь в компоненте и легко доступна с помощью this.state, то возникает большой соблазн добавить сюда же логику вроде подсчетов чего-либо или валидацию, чего не должно быть в компоненте. Возвращаясь к одному из предыдущих пунктов, это сильно затрудняет тестирование — вы не можете протестировать логику рендеринга, без путающейся под ногами бизнес-логики, и наоборот!

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

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

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

Каждый раз, добавляя компоненту состояние, убедитесь, что это действительно нужно. Иначе, однажды начав, будет сложно перестать добавлять все новые и новые вещи, и все выйдет из под контроля раньше, чем вы это обнаружите!

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

В первом пункте я сказал, что React это только отображение. Тогда возникает очевидный вопрос “Где должны храниться состояние и логика?”. Я рад, что вы спросили!

Возможно вы уже слышали о Flux, который представляет собой стиль/шаблон/архитектуру для создания web-приложений, в основном использующих для рендеринга React. Существует несколько фреймворков, воплощающих идеи Flux, но я без сомнения могу порекомендовать Redux.js *.

Я планирую написать целый отдельный пост о функциях и преимуществах Redux, но пока просто порекомендую прочитать официальное руководство по ссылке выше. Вот очень краткое описание того, как работает Redux:

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

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

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

Есть несколько библиотек, которые очень хорошо дополняют Redux, советую их тоже использовать:

  • Immutable.js — немутабельные структуры данных для JavaScript! Используйте их для хранения состояния, чтобы быть уверенным, что оно не меняется там, где не должно, а также чтобы сохранить функциональную чистоту редьюсеров
  • redux-thunk — используется когда нужно, чтобы действия (actions) имели какой-либо побочный эффект в дополнение к обновлению состояния приложения. Например, вызов REST API, или установка маршрутов (routes), или даже вызов других действий.
  • reselect — используется для создания составных, лениво исполняемых отображений. Например для конкретного компонента вам может потребоваться:
  • 1. вставить только определенную часть глобального состояния, а не полностью
  • 2. вставить дополнительные производные данные, например итого или результаты валидации данных, не сохраняя все это в состоянии

Не обязательно использовать все это с первого дня. Я советую применить Redux и Immutable.js как только у вас появится какое-нибудь состояние, reselect — когда у вас появятся производные данные, и redux-thunk — когда у вас появится маршрутизация или асинхронные действия. Лучше начать их использовать раньше, чем позже — это избавит вас от лишней работы для внедрения их в дальнейшем.

*В зависимости от того, кого вы спросите, Redux может быть или не быть “настоящей” реализацией Flux. Лично я считаю, что он достаточно хорошо соответствует ключевым идеям, чтобы называться Flux-фреймворком, но, как бы то ни было, это все вопрос терминологии.

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

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

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

  • это может отловить ошибку на ранней стадии, предотвращая глупые ошибки
  • если вы воспользуетесь isRequired, то вам не придется так часто делать проверку на undefined или null
  • это также выполняет роль документации, освобождая читателя от необходимости поиска по всему компоненту, с целью понять какие props ему нужны

Этот список выглядит так, будто составлен апологетом языков со статической типизацией. Лично я предпочитаю динамическую типизацию, потому что она делает разработку более легкой и быстрой. Но я пришел к тому, что propTypes дает типобезопасность, не добавляя излишнюю сложность. Честно говоря, я не вижу причины не использовать propTypes всегда, когда это возможно.

Еще один совет — сделайте так, чтобы тесты падали при любых ошибках, связанных с propTypes. Вот грубоватое, но рабочее решение:

7. Используйте поверхностный рендеринг

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

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

Когда я тестирую компоненты, чаще всего я пишу следующие виды юнит-тестов:

Логика рендеринга

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

Мы можем протестировать его следующим образом:

Проще простого! Стоит обратить внимание, что API для поверхностного рендеринга немного сложнее, чем показано в этом примере. Функция shallowRender это наша самописная вспомогательная обертка вокруг реального API, облегчающая его использование.

Возвращаясь к нашему компоненту ListOfNumbers, вот так мы можем протестировать, что обход массива происходит корректно:

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

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

Наиболее частый аргумент против такого подхода к тестированию это излишнее повторение props.children.props.children... Хотя это и не самый аккуратный код, лично я заметил, что когда мне начинает надоедать часто писать props.children в одном тесте — это признак того, что компонент слишком большой, сложный или глубоко вложенный, и его нужно разделить на более мелкие.

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

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

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

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

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

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

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

  1. Рендерите все дерево компонентов (вместо поверхностного рендеринга)
  2. Работайте непосредственно с DOM (используя React TestUtils или jQuery), чтобы определить элементы, которые вас непосредственно интересуют, и затем:
  3. Проверяйте их HTML атрибуты, содержимое или…
  4. Эмулируйте события DOM и затем проверяйте побочные эффекты (изменения в DOM или в маршрутах, вызовы AJAX и т.п.)

По поводу TDD

Вообще, я не использую TDD при написании React-компонентов. Во время работы над компонентом, я часто “перетряхиваю” его структуру, пытаясь написать как можно более простые HTML и CSS, которые выглядят правильно в любом из поддерживаемых мной браузеров. А поскольку мой подход к модульному тестированию компонентов, как правило, завязан на их структуру, то мне придется постоянно чинить тесты, что будет пустой тратой времени.

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

В этом разделе я говорил только о тестировании компонентов, поскольку для тестирования остальной части Redux-приложений не требуется какая-либо специфическая информация. Как у фреймворка, у Redux совсем немного “магии”, скрытой под капотом. И я считаю, это сокращает потребность в каком-либо чрезмерном подготовительном процессе к тестированию. Все что есть, это всего лишь давно знакомые, простые функции (много из них чистые), что является настоящим глотком свежего воздуха, когда дело доходит до тестирования.

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

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

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

В довершение ко всему, мы используем Webpack для сборки нашего кода, и NPM для управления зависимостями. И теперь мы полностью соответствуем новомодным трендам в JavaScript :)

(Прим. пер.: сейчас рекомендуется использовать yarn вместо NPM, по ряду причин)

9. Используйте dev tools для React и Redux

Говоря о сопутствующем инструментарии, у React и Redux есть достаточно крутые инструменты разработчика. React dev tools дает возможность проинспектировать отрендеренное дерево компонентов, что чрезвычайно полезно, когда необходимо увидеть что же в конечном итоге получилось в браузере. Redux dev tools гораздо более выразительны и позволяют увидеть каждое произошедшее действие, изменение состояния, которое оно вызвало, и даже дает возможность перемещаться назад во времени! Вы можете добавить его как dev-зависимость, или как браузерное расширение.

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

10. Бонус от переводчика. Используйте React Create App

react-create-app это утилита командной строки, которая дает возможность создать пустое приложение с базовой структурой директорий, встроенным и настроенным Webpack, Babel и Eslint, со всеми зависимостями. Вам остается только запустить его командой yarn start и сфокусироваться на коде и бизнес-логике вашего приложения, не погружаясь в дебри настройки окружения. Если же вдруг потребуется изменить стандартную конфигурацию, то и такая возможность есть (команда eject).

Заключение

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

Спасибо за внимание!