Хабрахабр

[Перевод] 12 советов для тех, кто использует Redux при разработке React-приложений

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

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

Он бывал в подобных ситуациях. Автор этого материала просит читателя поверить ему на слово. Поначалу он думал, что всё у него получается превосходно. Так, ему досталось несколько задач, которые нужно было решить за определённое время. Он знал о том, как использовать Redux, о том, как наладить нормальное взаимодействие компонентов пользовательского интерфейса. Источником подобных мыслей стало то, что его веб-приложение, после внесения изменений, продолжало работать, и при этом продолжало работать быстро. Он чувствовал себя неуязвимым. Ему казалось, что он глубоко понимает концепции редьюсеров и действий.

Но тут подкралось будущее.

После этого проект вышел из-под контроля. Через пару месяцев работы над приложением в него было добавлено более 15 новых возможностей. Почему так случилось? Код, в котором использовалась библиотека Redux, стало очень тяжело поддерживать. Разве поначалу не казалось, что проект ожидает долгая и безоблачная жизнь?

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

Библиотека Redux, если правильно использовать её в больших проектах, помогает, по мере роста таких проектов, сохранять их код в поддерживаемом состоянии.

Здесь будут даны 12 советов для тех, кто хочет разрабатывать масштабируемые React-приложения с использованием Redux.

1. Не размещайте код действий и констант в одном месте

Вы могли столкнуться с некоторыми руководствами по Redux, в которых константы и все действия размещают в одном и том же месте. Однако подобный подход, по мере роста приложения, быстро может привести к появлению проблем. Константы нужно хранить отдельно, например, в ./src/constants. В результате для поиска констант придётся заглядывать лишь в одну папку, а не в несколько.

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

В такой ситуации добиться высокого уровня поддерживаемости кода можно, организовав действия следующим образом: Предположим, вы разрабатываете аркадную или ролевую игру и создаёте классы warrior (воин), sorceress (волшебница) и archer (лучник).

src/actions/warrior.js
src/actions/sorceress.js
src/actions/archer.js

Гораздо хуже будет, если всё попадёт в один файл:

src/actions/classes.js

Если приложение становится очень большим, то, возможно, ещё лучше будет воспользоваться примерно такой структурой разбиения кода по файлам:

src/actions/warrior/skills.js
src/actions/sorceress/skills.js
src/actions/archer/skills.js

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

src/actions/warrior/skills.js
src/actions/warrior/quests.js
src/actions/warrior/equipping.js
src/actions/sorceress/skills.js
src/actions/sorceress/quests.js
src/actions/sorceress/equipping.js
src/actions/archer/skills.js
src/actions/archer/quests.js
src/actions/archer/equipping.js

Вот как может выглядеть действие из файла src/actions/sorceress/skills для объекта sorceress:

import from '../constants/sorceress' export const castFireTornado = (target) => ({ type: CAST_FIRE_TORNADO, target,
}) export const castLightningBolt = (target) => ({ type: CAST_LIGHTNING_BOLT, target,
})

Вот содержимое файла src/actions/sorceress/equipping:

import * as consts from '../constants/sorceress' export const equipStaff = (staff, enhancements) => {...} export const removeStaff = (staff) => {...} export const upgradeStaff = (slot, enhancements) => { return (dispatch, getState, { api }) => { // Обратиться к слоту на экране обмундирования для того чтобы получить ссылку на посох волшебницы const state = getState() const currentEquipment = state.classes.sorceress.equipment.current const staff = currentEquipment[slot] const isMax = staff.level >= 9 if (isMax) { return } dispatch({ type: consts.UPGRADING_STAFF, slot }) api.upgradeEquipment({ type: 'staff', id: currentEquipment.id, enhancements, }) .then((newStaff) => { dispatch({ type: consts.UPGRADED_STAFF, slot, staff: newStaff }) }) .catch((error) => { dispatch({ type: consts.UPGRADE_STAFF_FAILED, error }) }) }
}

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

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

2. Не размещайте код редьюсеров в одном месте

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

const equipmentReducers = (state, action) => { switch (action.type) { case consts.UPGRADING_STAFF: return { ...state, classes: { ...state.classes, sorceress: { ...state.classes.sorceress, equipment: { ...state.classes.sorceress.equipment, isUpgrading: action.slot, }, }, }, } case consts.UPGRADED_STAFF: return { ...state, classes: { ...state.classes, sorceress: { ...state.classes.sorceress, equipment: { ...state.classes.sorceress.equipment, isUpgrading: null, current: { ...state.classes.sorceress.equipment.current, [action.slot]: action.staff, }, }, }, }, } case consts.UPGRADE_STAFF_FAILED: return { ...state, classes: { ...state.classes, sorceress: { ...state.classes.sorceress, equipment: { ...state.classes.sorceress.equipment, isUpgrading: null, }, }, }, } default: return state }
}

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

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

3. Используйте информативные имена переменных

Именование переменных, на первый взгляд, может показаться элементарной задачей. Но на самом деле эта задача может оказаться одной из самых сложных.

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

Случалось ли вам запускать чужую программу и обнаруживать, что работает она не так, как ожидается? Пытались ли вы когда-нибудь редактировать чужой код и сталкивались ли при этом со сложностями в понимании того, что именно делает этот код?

Я взялся бы на спор доказать то, что вы в таких случаях встречались с так называемым «грязным кодом».

Такое, к сожалению, происходит довольно часто. Если с подобным кодом приходится сталкиваться в крупных приложениях — то это просто кошмар.

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

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

Через 5-10 минут мне удалось найти код, ответственный за работу с нужными мне данными. Я поискал в коде по словам info, dataToSend, dataObject, и по другим, которые, в моём представлении, связаны с данными, получаемыми с сервера. В моём представлении объект, имеющий отношение к платежам, может содержать нечто вроде CVV-кода, номера кредитной карты, почтового индекса плательщика, и другие подобные сведения. Объект, в котором они оказывались, был назван paymentObject. Лишь три из них имели отношение к платежам: метод оплаты, идентификатор платёжного профиля и список кодов купонов. В обнаруженном мной объекте было 11 свойств.

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

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

import React from 'react' class App extends React.Component { state = { data: null } // Кого уведомляем-то? notify = () => { if (this.props.user.loaded) { if (this.props.user.profileIsReady) { toast.alert( 'You are not approved. Please come back in 15 minutes or you will be deleted.', { position: 'bottom-right', timeout: 15000, }, ) } } } render() { return this.props.render({ ...this.state, notify: this.notify, }) }
} export default App

4. Не изменяйте структуры данных или типы в уже настроенных потоках данных приложений

Одной из самых больших ошибок, которые я когда-либо совершал, было изменение структуры данных в уже настроенном потоке данных приложения. Новая структура данных принесла бы огромный прирост производительности, так как она использовала быстрые методики поиска данных в объектах, хранящихся в памяти, вместо перебора массивов. Но было слишком поздно.

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

Например, если нечто сначала было массивом, а потом стало объектом, это способно нарушить работу многих частей приложения. Какие последствия могут быть у подобного шага? Однако в подобных случаях всегда находится какой-нибудь фрагмент кода, на который влияет изменение, и о котором никто не помнит. Я сделал огромную ошибку, полагая, что способен помнить обо всех местах кода, на которые может повлиять изменение представления структурированных данных.

6. Используйте сниппеты

Раньше я был фанатом редактора Atom, но перешёл на VS Code из-за того, что этот редактор, в сравнении с Atom, оказался невероятно быстрым. И он, при его скорости, поддерживает огромное количество самых разных возможностей.

Это расширение позволяет программисту создавать собственные сниппеты для каждого рабочего пространства, используемого в некоем проекте. Если вы тоже пользуетесь VS Code — рекомендую установить расширение Project Snippets. Разница заключается в том, что при работе с Project Snippets в проекте создают папку .vscode/snippets/. Это расширение работает так же, как и встроенный в VS Code механизм Use Snippets. Выглядит это так, как показано на следующем рисунке.

Содержимое папки .vscode/snippets/

7. Создавайте модульные, сквозные и интеграционные тесты

По мере роста размеров приложения программисту становится всё страшнее редактировать код, который не покрыт тестами. Например, может случиться так, что некто отредактировал код, хранящийся в src/x/y/z/, и решил отправить его в продакшн. Если при этом внесённые изменения влияют на те части проекта, о которых программист не подумал, всё может закончиться ошибкой, с которой столкнётся реальный пользователь. Если в проекте имеются тесты, программист узнает об ошибке задолго до того, как код попадёт в продакшн.

8. Проводите мозговые штурмы

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

А зачем, кстати, вообще проводить мозговые штурмы в ходе разработки приложений?

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

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

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

9. Создавайте макеты приложений

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

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

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

10. Планируйте поток данных в приложениях

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

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

11. Используйте функции доступа к данным

По мере роста размеров приложения растёт и количество его компонентов. А когда растёт количество компонентов, то же самое происходит и с частотой использования селекторов (react-redux ^v7.1) или mapStateToProps. Предположим, вы обнаруживаете, что ваши компоненты или хуки часто обращаются к фрагментам состояния в различных частях приложения с использованием конструкции наподобие useSelector((state) => state.app.user.profile.demographics.languages.main). Если так — это значит, что вам нужно подумать о создании функций доступа к данным. Файлы с такими функциями стоит хранить в общедоступном месте из которого их могут импортировать компоненты и хуки. Подобные функции могут быть фильтрами, парсерами, или любыми другими функциями для трансформации данных

Вот несколько примеров.

Например, в src/accessors может присутствовать такой код:

export const getMainLanguages = (state) => state.app.user.profile.demographics.languages.main

Вот версия с использованием connect, которая может быть расположена по пути src/components/ViewUserLanguages:

import React from 'react'
import { connect } from 'react-redux'
import { getMainLanguages } from '../accessors' const ViewUserLanguages = ({ mainLanguages }) => ( <div> <h1>Good Morning.</h1> <small>Here are your main languages:</small> <hr /> {mainLanguages.map((lang) => ( <div>{lang}</div> ))} </div>
) export default connect((state) => ({ mainLanguages: getMainLanguages(state),
}))(ViewUserLanguages)

Вот версия, в которой применяется useSelector, находящаяся по адресу src/components/ViewUserLanguages:

import React from 'react'
import { useSelector } from 'react-redux'
import { getMainLanguages } from '../accessors' const ViewUserLanguages = ({ mainLanguages }) => { const mainLanguages = useSelector(getMainLanguages) return ( <div> <h1>Good Morning.</h1> <small>Here are your main languages:</small> <hr /> {mainLanguages.map((lang) => ( <div>{lang}</div> ))} </div> )
} export default ViewUserLanguages

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

12. Управляйте потоком данных в свойствах с помощью деструктурирования и синтаксиса spread

Каковы преимущества использования конструкции props.something перед конструкцией something?

Вот как это выглядит без использования деструктурирования:

const Display = (props) => <div>{props.something}</div>

Вот — то же самое, но уже с использованием деструктурирования:

const Display = ({ something }) => <div>{something}</div>

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

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

const Display = ({ something = 'apple' }) => <div>{something}</div>

Возможно, раньше вы видели что-то подобное следующему примеру:

const Display = (props) => ( <Agenda {...props}> {' '} // перенаправление других свойств компоненту Agenda <h2><font color="#3AC1EF">Today is {props.date}</font></h2> <hr /> <div> <h3><font color="#3AC1EF">▍Here your list of todos:</font></h3> {props.children} </div> </Agenda>
)

Подобные конструкции непросто читать, но это — не единственная их проблема. Так, здесь имеется ошибка. Если приложение выводит и дочерние компоненты, то props.children отображается на экране дважды. Если работа над проектом ведётся в команде и члены команды недостаточно внимательны, вероятность возникновения подобных ошибок довольно-таки высока.

Если вместо этого деструктурировать свойства, то код компонента окажется понятнее, а вероятность возникновения ошибок снизится:

const Display = ({ children, date, ...props }) => ( <Agenda {...props}> {' '} // перенаправление других свойств компоненту Agenda <h2><font color="#3AC1EF">Today is {date}</font></h2> <hr /> <div> <h3><font color="#3AC1EF">▍Here your list of todos:</font></h3> {children} </div> </Agenda>
)

Итоги

В этом материале мы рассмотрели 12 рекомендаций для тех, кто разрабатывает React-приложения с использованием Redux. Надеемся, вы нашли здесь что-то такое, что вам пригодится.

Уважаемые читатели! Какие советы вы добавили бы к тем, что приведены в этой статье?

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть