Как часто рано или поздно при решении новой задачи приходит мысль: а нельзя ли для неё переиспользовать код из реализованной ранее аналогичной задачи? Думаю, что в такой момент нами движет что-то среднее между ленью и здравым смыслом. Ну не писать же всё с нуля? Далее появляется подлая мыслишка: а может, просто «скопипастить» и не заморачиваться?)
Концепция повторного использования кода. Зачем нужно писать повторно используемый код?
Все мы в начале своего «программистского» пути начинали переиспользовать код с ctrl+C ctrl+V. Некоторые продолжают так делать и дальше, аргументируя свои действия: «Ведь код и так будет работать. Зачем тратить время на что-то ещё? Задача решена, давайте следующую!» Соглашусь, код будет работать, но в дальнейшем такой подход неизбежно приведёт к проблемам при изменении и поддержке кодовой базы. Если вам вдруг понадобится поменять логику в коде, который был многократно скопирован, а не грамотно переиспользован, то вам потребуется найти все скопированные фрагменты и внести изменения в каждый из них, а потом ещё и поправить тесты для каждого из этих фрагментов. Вы ведь пишете тесты?
Поэтому, на мой взгляд, лучше всё-таки тратить немного больше времени в начале, создавая код, который можно легко повторно использовать, чем тратить уйму времени на поддержку дублированного кода в будущем.
Теперь поговорим подробнее о подходе с повторным использованием кода. Основная концепция этого подхода состоит в том, что при проектировании компоненты разбивают сложные приложения на независимые части, которые проще реализовать. Я бы посоветовала подходить к повторному использованию кода не как к специальному процессу, а как к стратегии развития. То есть мы должны изначально создавать компоненты с учётом их повторного использования в будущем. В первую очередь мы прорабатываем архитектуру, а потом уже пишем код. На первый взгляд, извлечение компонентов может показаться лишней и монотонной работой. Но когда у нас становится всё больше повторно используемых компонентов, это быстро окупается, ведь можно взять уже готовый компонент для решения своей задачи. Особенно заметно это становится в больших приложениях с обширной кодовой базой.
Выделение компонентов способствует созданию хорошей архитектуры. Код, созданный из качественно спроектированных компонентов, намного легче читать и воспринимать, а отсутствие дублирования кода позволит упростить багфикс, так как правки нужно будет вносить только в одном месте, а не искать по всему проекту нужные строчки кода. Чем короче код, тем он надёжнее, в нём сложнее запутаться и проще увидеть ошибку. Не бойтесь разделять компоненты на более мелкие части. Недаром существует такой принцип, как DRY: «Don’t repeat yourself».
Как понять, что нужно выделить компонент? Первым делом стоит обратить внимание на часто используемые части пользовательского интерфейса, например, на кнопку или панельку, имеющие одинаковую функциональность. А если у нас получается сложный компонент – это уже хороший кандидат для того, чтобы выделить в нём несколько подкомпонентов для упрощения работы с ним.
Теперь давайте перейдём к React-компонентам.
Что такое компоненты React?
Компоненты React – это самодостаточные элементы, которые можно использовать на странице любое количество раз. Во многом компоненты ведут себя как обычные JavaSript функции. Они принимают входные данные, так называемые props, и возвращают React-элементы, описывающие то, что мы хотим увидеть на экране.
Проще всего объявить React-компонент как стрелочную функцию.
Пример 1:
const Button = (props) => (
<button onClick={props.onClick}>Click me!</button>
)
В данном случае компонент принимает в качестве параметра props с обработчиком клика и возвращает React-элемент в виде кнопки. Компоненты такого типа называются функциональными.
Также компонент можно создать путём расширения класса React.Component. Такие компоненты называют классовыми, у них есть доступ к методам жизненного цикла и состоянию.
Перепишем наш функциональный компонент в виде классового.
Пример 2:
class Button extends React.Component {
constructor(props) {
super(props);
}
render() {
<button onClick={props.onClick}>Click me!</button>
}
}
Важно отметить, что компонент не должен менять свои props вне зависимости от того, функциональный он или классовый. Это позволяет гарантировать, что компонент будет возвращать один и тот же результат для одинаковых аргументов.
Также React-компоненты можно разделять по наличию состояния. Состояние представляет собой объект, который хранит динамические данные компонента. На основе изменения своего состояния компонент перерисовывается и может передавать свойства своего состояния в дочерние компоненты в качестве props. Таким образом, компоненты без состояния называют stateless компонентами, а с состоянием – stateful.
Приведённые выше примеры представляют собой stateless компоненты. Попробуем добавить объект состояния в классовый компонент.
Пример 3:
class Button extends React.Component {
constructor(props) {
super(props);
this.state = { clickNumber : 0};
}
const onClick = () => {
this.setState({ count: this.state.clickNumber + 1 });
props.onClick();
}
render() {
<button onClick={() => onClick()}>You clicked { this.state.clickNumber } times!</button>
}
}
В данном примере мы добавили объект состояния в класс Button, который отслеживает изменение количества нажатий на кнопку и отображает актуальную информацию на странице.
Часто говорят, что stateless компоненты – это только функциональные компоненты, а stateful – классовые, но это не так. В действительности наличие состояния не зависит от того, является ли компонент классовым или функциональным. Рассматривая пример 2, мы убедились, что классовые компоненты могут не иметь состояние.
А как определить состояние в функциональных компонентах? До изобретения хуков в React функциональные компоненты не могли иметь состояние, но благодаря хуку useState это стало возможно. Хук – это специальная функция, которая позволяет использовать возможности React без написания классов.
Давайте добавим состояние в наш функциональный компонент из примера 1.
Пример 4:
function Button = () => {
const [clickNumber, setClickNumber] = useState(0);
return (
<button onClick={() => setClickNumber (clickNumber + 1)}>
You clicked { this.state.clickNumber } times!
</button>
)
}
Мы получили ту же самую кнопку, которая при клике увеличивает значение того, сколько раз на неё кликнули. Вызов useState возвращает нам два значения: текущее состояние и функцию, которая обновляет это состояние. Это очень похоже на то, как мы используем состояние в классовых компонентах.
Можно увидеть, что использование классового компонента длиннее практически в два раза. Лучше использовать функциональные компоненты с применением хуков, потому что это проще выглядит, надёжнее, удобнее в плане разделения состояний или использования других хуков, а также никак не отразится на вашей производительности.
Итак, почему лучше использовать функциональные компоненты с хуком useState,
чем классовые?
Во-первых, проще для понимания, потому что это обычные функции.
Во-вторых, не нужно иметь дело с this. Порой не очень удобно связывать функции, когда нужно использовать обработчик событий. Часто возникают проблемы, не все разбираются в том, как работает окружение.
В-третьих, синтаксис короче, меньше шансов на появление багов. Также состояние более детализировано, в классовых компонентах у нас один большой объект состояний. Когда мы что-то изменяем, обновление переменной состояния всегда происходит путём замещения её значения новым объектом, а не изменением текущего состояния с помощью слияния. С хуком React мы можем разделить состояние и обновлять отдельно каждое значение.
Но это всё теория, на практике возникает много вопросов, попробуем разобрать некоторые из них:
Допустим, что наша команда решила создавать и использовать повторно используемые компоненты. Как это повлияет на нашу разработку?
С одной стороны, вы будете тратить больше времени на разработку, по крайней мере, вначале. С первого взгляда выделение повторно используемых компонентов может показаться тривиальной задачей, но в процессе написания кода могут обнаружиться зависимости между компонентами, которые помешают их универсальному использованию и сделают невозможным их отделение. А иногда при добавлении новых фичей или дальнейшем продумывании архитектуры приходится добавлять новые свойства в компонент или изменять его логику, при этом поддерживая работоспособность старого кода. Всё это значительно усложняет процесс создания универсального компонента.
С другой стороны, такой подход сэкономит ваше время при разработке новых компонентов за счёт переиспользования компонентов, написанных ранее. Ведь когда вы будете разрабатывать аналогичный компонент, вам не потребуется делать это с нуля. У вас уже есть компоненты, которые вы можете переиспользовать. Также у вас значительно упростится багфикс, так как при возникновении бага вам потребуется сделать исправление только в одном месте. Рассмотрим ситуацию: у вас в проекте два похожих компонента, выполняющих одну и ту же задачу, но созданных разными людьми в разных частях проекта. В одном компоненте вы нашли ошибку и исправили её, но во втором ошибка так и останется. А в случае повторного использования компонента та же ошибка в совсем другом месте кода была бы исправлена автоматически.
Другим важным плюсом такого подхода является хорошая структуризация кода, облегчающая его восприятие и понимание. Когда другой разработчик видит код с хорошим названием компонентов, из которых состоит один большой родительский компонент – он намного быстрее поймёт код, и ему не придётся разбираться в функциях и тегах. Такой подход нам напоминает выделение самостоятельных частей в функции для создания чистого кода. При этом, если каждый программист будет вести разработку таким образом, то вы получите одинаковый стиль написания кода, что, естественно, ускоряет его понимание другими разработчиками.
Какие есть недостатки повторного использования кода?
Подход с повторным использованием компонентов даёт серьёзный профит вашему проекту, но только если вся команда чётко следует ему и уделяет достаточно времени тестированию. Без этого повторно используемые компоненты часто становятся причиной нестабильности приложений, потому что при багфиксе разработчик исправляет код в соответствии с тем сценарием использования компонента, который актуален для его конкретной ситуации. Он не думает о том, что этот компонент ещё используется в десятке других сценариев. Его правка может отразиться совершенно непредсказуемым образом на незнакомых ему частях приложения. Такие ошибки очень трудно найти, потому что их никто не проверяет.
Рассмотрим другую ситуацию. В процессе доработки и развития приложения иногда возникает потребность добавить новую функциональность в этот компонент. Самое простое, что разработчик находит возможным сделать, - изменение кода этого компонента, например, добавление новых props или новой логики. Вся эта логика постепенно накапливается в этом компоненте, он обрастает десятками разных props и сложных условий. Всё это становится крайне хрупким и тяжёлым для отладки.
Как избежать проблем с переиспользуемыми компонентами?
Вы должны расширять функциональность, а не менять её.
Для того чтобы не изменять компонент, который уже выделили как отдельную единицу, лучше использовать компоненты высшего порядка или функции оболочки, в которых будет реализована дополнительная логика. Таким образом, вы не сломаете уже имеющийся функционал.Делать максимально простые компоненты, выделяя наиболее общие признаки.
Чтобы создать универсальный компонент, который везде подошёл бы, нужно выделять максимально общие признаки. Тогда вы сможете избежать ситуации, когда для одной цели вам нужно что-то изменить, а для другой оставить так же. Для повторного использования рекомендуется использовать небольшие stateless компоненты. Отсутствие состояния и минимум логики позволит упростить его использование. Такие компоненты нужно делать максимально похожими на html-теги, чтобы они принимали все пропсы html. Если компонент основан на теге div и ведёт себя как тег div, то велика вероятность, что у вас получится расширить использование этого компонента, добавив обработчики событий, не изменяя сам компонент.Композиция компонентов.
Если у вас возникает потребность создать какой-то более сложный компонент, хорошим правилом будет составлять этот сложный компонент из более маленьких компонентов, то есть применять композицию. Это нужно для того, чтобы модифицировать его под другой сценарий, когда появится такая необходимость. В такой ситуации вы можете просто создать другую композицию, переиспользуя уже существующие компоненты, но при этом создать что-то новое.Повторно используемые компоненты должны быть явно выделены в кодовой базе.
Выделенные для повторного использования компоненты лучше хранить в отдельном месте в проекте, например, создать папку reusable. Это поможет понять другим разработчикам, какой именно код создавался с учётом повторного использования. Разработчики быстрее смогут найти компонент для переиспользования и будут понимать, что для этих компонентов надо поддерживать совместимость и следить за тем, чтобы не поломать существующий код при доработке функциональности.Не забываем про тесты!
Очень важно писать тесты, чтобы проверить, не поломалась ли функциональность. Код, который мы используем повторно, требует более тщательного тестирования. Когда мы меняем повторно используемые компоненты, мы должны убедиться в работоспособности нашего приложения. Для этого код нужно протестировать юнит-тестами и скриншот-тестами, это поможет обеспечить его надёжность.
Почему нельзя сделать состояние с помощью замыкания без использования хука?
Проблема в том, что при изменении состояния нам нужно перерендерить компонент. Просто так изменять состояние не имеет смысла. А как мы знаем, в функциональных компонентах нет возможности изменения состояния с перерендерингом. Эта возможность зашита в хуках React. Они тоже используют замыкание для хранения значения, но именно при изменении состояния они «триггерят» React, чтобы он перерисовал компонент.
Какие компоненты сейчас лучше использовать: функциональные или классовые?
Лучше использовать функциональные компоненты. Всё идёт к тому, что классовые компоненты объявят deprecated в будущем. Это, скорее всего, произойдёт нескоро, потому что классовые компоненты всё ещё используются в очень большой кодовой базе. Но даже если их вообще не исключат из библиотеки, всё равно вектор развития библиотеки React будет направлен на функциональные компоненты с хуками, а классы будут постепенно умирать. Новый код сейчас лучше писать на функциональных.
Подведём итоги
Повторное использование кода является важной составляющей хорошей архитектуры. Использование данного подхода даёт огромные плюсы при расширении кодовой базы и багфиксе. Чтобы этот подход действительно работал, уделяйте побольше времени на продумывание архитектуры компонентов и написание тестов. Это поможет обезопасить вас от большинства ошибок в будущем.
При этом нужно помнить, что повторное использование кода — это хорошо, но не стоит слишком этим увлекаться, чтобы не выстрелить себе же в ногу. Если вы не уверены, что этот компонент достаточно общий и подходящий для выделения в качестве повторно используемого компонента, то оставьте эту навязчивую идею сделать всё идеально переиспользуемым!