Оригинальный текст этого перевода:Five Tips for Working with Redux in Large Applications
Заказ переводчика
Причина, по которой я перевел эту статью, заключается в том, что советы, данные в этой статье, совпадают с моей практикой в реальных проектах и являются более точными и лучшими. Поэтому я хочу поделиться им со всеми вами.
По мере роста размера проекта ограничения вводной документации и кода проекта на уровне учебника постепенно станут очевидными, и вы столкнетесь с проблемами, с которыми не столкнулись бы в небольшом приложении. Что более фатально, если вы хотите решить эти проблемы, вам нужно внести коррективы в код всего приложения. Таким образом, лучше сознательно использовать передовой опыт там, где создается проект, чтобы предотвратить проблемы в будущем.
Эта статья не подходит для новичков в Redux, я надеюсь, что вы разработали несколько законченных приложений или, по крайней мере, прочитали эту статью, когда разрабатываете свое первое приложение, чтобы получить лучший опыт.
Совет, данный в этой статье, более или менее определенно упоминается в официальной документации Redux или официальной документации React. Но документация слишком большая, а запись слишком глубокая, чтобы перегружать содержание. Если вы еще не прикоснулись к ним, по крайней мере, эта статья не позволит вам пропустить их снова.
Некоторые неудобные перевод, или перевод неловко, или место, где распознанный технический словарь все еще хранится в оригинальном тексте. Следующее официально начинается
текст
Redux — отличный инструмент для управления состоянием приложения. Его однонаправленный поток данных и неизменяемые функции состояния упрощают отслеживание изменений состояния. Каждое изменение состояния вызвано отправленным действием, заставляющим функцию редуктора возвращать новое состояние. Многие пользовательские интерфейсы на нашем сайте, созданные с помощью Redux, должны иметь дело с большими объемами данных и сложными взаимодействиями, поскольку пользователям необходимо управлять своей рекламой или обновлять информацию об инвентаре на платформе через эти интерфейсы. В процессе разработки этих интерфейсов мы узнали некоторые правила и приемы, которые упрощают обслуживание Redux. Следующие несколько пунктов, которые мы обсудим, будут полезны тем студентам, которые используют Redux для разработки крупномасштабных приложений типа интеграции данных.
- Используйте индексы и селекторы для сортировки и доступа к данным
- Изолировать объекты данных от состояния редактирования и другого состояния пользовательского интерфейса.
- Как разделить состояние между несколькими представлениями
- Повторное использование редукторов в разных штатах
- Лучшие практики для подключения компонентов к состоянию Redux
1. Используйте индекс (индекс) для хранения данных, используйте селектор (селектор) для доступа к данным
Выбор правильной структуры данных имеет решающее значение для организации и выполнения вашего приложения. Использование индекса для хранения сериализуемых данных из интерфейса приносит много преимуществ. Индекс относится к идентификатору объекта в объекте, нам нужно хранить, и значение является самим объектом. Этот режим аналогичен использованию карты HASH для хранения данных, которые могут сэкономить время для поиска. Это может прийти как неудивительно для кого-то, кто имеет опыт в redux. На самом деле, автор Redux, Дэн Абрамоб, в егоУчебник по редуксуЭта структура данных также рекомендуется в
Представьте, что вы запрашиваете список данных из интерфейса REST, например из/users
Служить. Мы решили просто хранить данные этого простого массива в состоянии точно так же, как возвращает интерфейс. Так что же происходит, когда вам нужно получить определенную информацию о пользователе из объекта? Нам нужно перебрать всех пользователей в состоянии state. Это может занять много времени, если пользователей слишком много. Или что, если вы хотите отслеживать подмножество пользователей, например, выбранных пользователей или невыбранных пользователей? Либо сохраните пользователей как два отдельных изолированных массива, либо отслеживайте индексы выбранных и невыбранных пользователей (массивов) в массиве.
Вместо этого мы решили реорганизовать код, чтобы использовать индексы для хранения данных. В редюсере данные должны храниться так:
{
"usersById": {
123: {
id: 123,
name: "Jane Doe",
email: "jdoe@example.com",
phone: "555-555-5555",
...
},
...
}
}
Но как такая структура данных помогает нам решать эти проблемы? Если вам нужно найти определенный пользовательский объект, просто получите к нему доступ следующим образом:const user = state.usersById[userId]
, Этот метод не нужно проходить весь массив, экономит время и упрощает извлечение кода
На этом этапе вам может быть интересно, как представить данные в этой структуре данных в простой список пользователей. Для этого нам нужен селектор, функция, которая принимает состояние и возвращает данные. Пример простого селектора для получения всех пользователей в состоянии:
const getUsers = ({ usersById }) => {
return Object.keys(usersById).map((id) => usersById[id]);
}
В коде представления вызовите функцию селектора, чтобы создать список пользователей. Затем выполните итерацию по этим пользователям, чтобы отобразить представление. Мы также можем написать другую функцию для получения выбранного пользователя из состояния
const getSelectedUsers = ({ selectedUserIds, usersById }) => {
return selectedUserIds.map((id) => usersById[id]);
}
Селектор режима Улучшение ремонтопригодности одного и того же кода. Через некоторое время представить, возможно, нам нужно изменить структуру состояния (форма). Если нет, то селектор, код должен быть обновлен в ответ на все представления модифицированной структуры состояния. С увеличением зрения сборки бремя государственной структуры изменит драматический рост. Чтобы избежать этой проблемы, в просмотре мы используем селектор для доступа к состоянию, если состояние базовой структуры изменилось, нам просто нужно обновить селекторы, чтобы обеспечить правильность способа состояния доступа. Все потребительские компоненты все равно будут получать необходимые данные без необходимости внесения изменений. По всем этим причинам широкомасштабное применение redux выиграет от селектора режима индексации и хранения в
2. Отделите стандартное состояние от состояний просмотра и редактирования.
Реальному приложению Redux обычно необходимо запрашивать некоторые данные из другого сервиса, например из интерфейса REST. Когда данные получены, действие будет инициировано только что полученными данными.Мы склонны возвращать данные из службы под названием «стандартное состояние» (каноническое состояние).. То есть данные в состоянии из базы. Состояние также включает другие типы данных, такие как состояние компонента или состояние приложения в целом. Когда стандартные данные впервые извлекаются из API, делается попытка сохранить их в том же редюсере, что и остальную часть состояния страницы. Хотя этот метод был бы удобным, его было бы очень сложно масштабировать, когда вам нужно запрашивать больше типов данных из разных источников.
Вместо этого мы изолируем стандартное состояние в отдельный файл редьюсера. Такой подход способствует лучшей организации и модульности кода. Масштабирование файла редуктора по вертикали (увеличение количества строк в одном файле) более удобно в сопровождении, чем масштабирование редуктора по горизонтали (добавление большего количества файлов редуктора дляcombineReducers
call) плохо ремонтопригодна. Разделение редукторов на отдельные файлы также облегчит их повторное использование. Кроме того, это препятствует разработчикам добавлять нестандартное состояние в редукторы объектов данных.
Почему бы не сохранить другое состояние вместе со стандартным состоянием? Представьте, что у нас есть тот же список данных о пользователях, которые запрашиваются из интерфейса REST. Используя шаблон индекса из предыдущего раздела для хранения, вы можете хранить данные в редюсере следующим образом:
{
"usersById": {
123: {
id: 123,
name: "Jane Doe",
email: "jdoe@example.com",
phone: "555-555-5555",
...
},
...
}
}
Теперь представьте пользовательский интерфейс, который позволяет пользователю редактировать представление. Когда пользователь щелкает значок редактирования, нам нужно обновить состояние представления, чтобы представление отображало элементы управления редактирования для пользователя. Мы решили объединить состояние просмотра со стандартным состоянием, добавив новое поле в каждый объект индекса.isEditing
,так:
{
"usersById": {
123: {
id: 123,
name: "Jane Doe",
email: "jdoe@example.com",
phone: "555-555-5555",
...
isEditing: true,
},
...
}
}
После редактирования нажмите кнопку отправки, и изменения будут переданы обратно в службу REST с помощью метода PUT. Сервис возвращает новое состояние объекта. Но как внедрить новое стандартное состояние в магазин? Если вы просто назначите новый объект на основе индекса,isEditing
Логотипа больше нет. Итак, теперь вам нужно вручную указать, какие поля в возврате интерфейса нужно объединить в хранилище. Это усложняет логику обновления. У вас может быть несколько логических значений, строк, массивов или других новых полей, необходимых для пользовательского интерфейса, вставленных в стандартное состояние. В этом сценарии может быть легко добавить действие для обновления стандартного состояния, но легко забыть сбросить поля пользовательского интерфейса в объекте и создать недопустимое состояние. Поэтому мы должны хранить стандартное состояние в отдельном редюсере хранилища, а действия делать простыми и легко отслеживаемыми.
Еще одним преимуществом разделения состояния редактирования является то, что если пользователь отменит редактирование, мы можем легко вернуться к стандартному состоянию. Представьте, что пользователь щелкнул значок редактирования и отредактировал поля имени и адреса электронной почты, теперь он не хочет сохранять эти изменения, поэтому нажимает кнопку отмены. Эта операция возвращает состояние представления к предыдущему состоянию. Но если стандартное состояние и состояние редактирования были объединены в одно, у нас больше нет старых данных, и мы вынуждены повторно запрашивать данные из интерфейса REST для получения стандартного состояния. Так что теперь сохраните состояние редактирования в другом месте. Общее состояние теперь выглядит так:
{
"usersById": {
123: {
id: 123,
name: "Jane Doe",
email: "jdoe@example.com",
phone: "555-555-5555",
...
},
...
},
"editingUsersById": {
123: {
id: 123,
name: "Jane Smith",
email: "jsmith@example.com",
phone: "555-555-5555",
}
}
}
Поскольку теперь есть стандартное состояние и состояние редактирования (копия стандартного состояния), очень легко выполнить откат после того, как пользователь нажмет кнопку «Отменить редактирование». Просто используйте стандартное состояние вместо состояния редактирования для отображения в представлении, и вам больше не нужно запрашивать интерфейс REST. Кроме того, мы по-прежнему можем отслеживать статус редактирования в магазине. Если вы решите продолжить использовать последнее редактирование, вам просто нужно снова нажать кнопку редактирования, и старые изменения снова отобразятся с состоянием редактирования. В целом, обеспечение отделения состояний просмотра и редактирования от стандартных состояний обеспечивает лучший опыт разработки с точки зрения организации кода и удобства сопровождения, а также лучший интерактивный опыт для пользователей, использующих формы.
3. Разумно разделяйте состояние между представлениями
Многие приложения начинаются с единого пользовательского интерфейса и единого хранилища. По мере роста функциональности и увеличения размера приложения нам необходимо управлять состоянием между различными представлениями и хранилищами. Чтобы масштабировать приложение Redux, может быть полезно создать редуктор верхнего уровня для каждой страницы. Каждая страница и редьюсер верхнего уровня соответствуют представлению в приложении. Например, представление списка пользователей будет запрашивать все пользовательские данные из интерфейса и сохранять их вusers
В редьюсере другая страница, отвечающая за отслеживание доменного имени, принадлежащего текущему пользователю, будет запрашивать данные из интерфейса доменного имени и сохранять их, состояние выглядит примерно так:
{
"usersPage": {
"usersById": {...},
...
},
"domainsPage": {
"domainsById": {...},
...
}
}
Это позволяет организациям, таким как страница и представление данных, развязывать и независимы (автономно). Каждая страница отслеживает свое состояние, файлы редуктора и даже может просматривать эхо-файлы (совмещенные). Когда приложения продолжают расширяться, мы можем обнаружить, что им нужна общая зависимость между различными представлениями в состоянии. При рассмотрении общего состояния учитывайте следующие моменты:
- Сколько представлений или редукторов зависит от этого фрагмента данных?
- Нужна ли мне копия этих данных для каждой страницы?
- Как часто обновляются данные?
Например, приложение необходимо отобразить информацию о текущей записи пользователя на каждой странице. Нам нужно получить информацию о пользователе из интерфейса и хранить его в магазине. Мы знаем, что каждая страница зависит от этих данных, поэтому эти данные не применяются к стратегии «Каждая страница, имеет независимый редуктор». Мы также знаем, что каждая страница не должна полагаться на копию этих данных, поскольку большинство страниц не запрашивают других пользователей, ни изменить текущий пользователь. Кроме того, эти данные о текущем зарегистрированном пользователю вряд ли могут измениться, если они не изменит себя на странице пользователя.
Затем кажется хорошей идеей разделить текущее состояние пользователя между страницами, поэтому мы извлекаем эти данные и помещаем их в собственный редьюсер на верхнем уровне. Теперь страница, которую пользователь посещает в первый раз, будет проверять, загружен ли редюсер текущего пользователя, и если нет, делать запрос из интерфейса. Эта информация о вошедшем в систему пользователе может быть просмотрена любым представлением, подключенным к хранилищу Redux.
Что мне делать с сценами, которые бессмыслены? Давайте рассмотрим еще один пример. Представьте, что каждое доменное имя, принадлежащее пользователю, также имеет определенное количество поддоменов. Мы добавим страницу списка подделки, чтобы показать все субдоменные имена пользователей, а страница списка доменных имен также предоставляет имена поддоменов, которые демонстрируют выбранные доменные имена. Существует две страницы для отображения данных в суб-домене сейчас. Мы также знаем, что доменные имена часто изменяются, и пользователи могут увеличиваться, удалять или редактировать доменные имена или субдомены в любое время. Каждая страница может потребоваться уникальная копия данных. Страница имени поддомена позволяет читать или записать через интерфейс поддомена, и можно включить данные к данным. Противоположное доменное имя принимается только после подмножества подмножества субдоменов (выбранные имена субдоменов доменного имени). Это, кажется, очень ясно, в котором общий статус поддомена на разных страницах в этом сценарии, похоже, не имеет хорошего выбора. Каждая страница должна хранить копию собственных данных поддоменов
4. Повторно используйте общие функции редуктора по состояниям
Написав несколько функций редьюсера, мы решили попробовать повторно использовать логику редюсера в разных местах состояния. Например, мы создали редьюсер для запроса информации о пользователе из интерфейса. Интерфейс возвращает только 100 единиц пользовательской информации за раз, но в системе их тысячи. Чтобы решить эту проблему, редьюсер должен отслеживать, какая страница данных отображается в данный момент. Логика запроса будет считывать эту информацию из редуктора и решать, какие параметры перелистывания страниц будут для следующего запроса (например, вызовpage_number
). Позже, при запросе списка доменных имен, та же самая логика, наконец, написана для запроса и хранения информации о доменном имени, но интерфейс и схема объектов другие, а поведение перелистывания страниц остается прежним. Умные разработчики поймут, что редукторы можно разделить на модули и использовать эту логику в любом редюсере, который должен переворачивать страницы.
Совместное использование логики редюсера в Redux требует небольшой хитрости. По умолчанию все функции редуктора вызываются при запуске нового действия. Если одна и та же функция редуктора используется несколькими функциями редуктора, это приведет к срабатыванию всех редукторов при запуске действия. Это не ожидаемое поведение повторно используемого редуктора. То есть, когда запрашивается список пользователей и получается 500 фрагментов данных, мы не хотим, чтобы количество списков доменных имен стало равным 500.
Мы рекомендуем два способа совместного использования, оба из которых используют область или префикс для специальной обработки типов действий. Первый способ требует передачи области действия в информации, переносимой действием. action использует тип действия, чтобы определить, какое поле в состоянии нужно изменить. Для наглядности представьте себе страницу с несколькими разными разделами, загружаемыми из интерфейса асинхронно. Статус отслеживания загрузки выглядит следующим образом:
const initialLoadingState = {
usersLoading: false,
domainsLoading: false,
subDomainsLoading: false,
settingsLoading: false,
};
В этом состоянии следующим шагом будет установка состояния загрузки каждого представления региона с помощью редьюсеров и действий. Мы могли бы написать четыре редуктора с разными действиями, каждый с отдельным типом действия. Но это целая куча повторяющегося кода. Вместо этого давайте попробуем использовать редукторы и действия с ограниченной областью действия и просто создадим один тип действия.SET_LOADING
, и функцию редуктора, подобную этой:
const loadingReducer = (state = initialLoadingState, action) => {
const { type, payload } = action;
if (type === SET_LOADING) {
return Object.assign({}, state, {
// sets the loading boolean at this scope
[`${payload.scope}Loading`]: payload.loading,
});
} else {
return state;
}
}
Вам также необходимо предоставить функцию создателя действия с областью действия для вызова редьюсера с областью действия. действие выглядит так:
const setLoading = (scope, loading) => {
return {
type: SET_LOADING,
payload: {
scope,
loading,
},
};
}
// example dispatch call
store.dispatch(setLoading('users', true));
Используя подобный редьюсер с ограниченной областью действия, проблема необходимости повторения одной и той же логики в разных функциях и действиях редуктора решается. Это значительно уменьшает количество повторяющегося кода и помогает нам писать меньшие файлы действий и редьюсеров. Если вам нужно добавить на страницу другое представление области, просто добавьте новый индекс в начальное состояние и вызовите его с другой областью действия.setLoading
. Это решение отлично работает, когда у вас есть несколько похожих полей, которые нужно обновлять одинаковым образом.
Также хотим разделить логику редуктора в разных частях состояния, вместо того, чтобы использовать один и тот же редюсер и действие для обновления нескольких полей в состоянии, мы хотим вызватьcombineReducers
Подключаемое повторное использование функций редуктора. Затем вам нужно вернуть функцию редуктора с префиксом типа, вызвав фабричную функцию редуктора.
Хорошим примером повторного использования логики редуктора является работа с информацией о переворачивании страниц. Возвращаясь к примеру запроса информации о пользователе, интерфейс может содержать тысячи пользователей, и интерфейс также будет предоставлять информацию о перелистывании страниц после пейджинга пользователей. Предположим, что полученный интерфейс возвращает что-то вроде этого:
{
"users": ...,
"count": 2500, // the total count of users in the API
"pageSize": 100, // the number of users returned in one page of data
"startElement": 0, // the index of the first user in this response
]
}
Если вы хотите данные на следующей странице, вам нужно инициироватьstartElement=100
Получить запрос параметров. Мы просто создаем редьюерную функцию для каждого дилингового сервиса, но это означает повторение той же логики в вашем коде. Вместо этого вы можете создать отдельный флип-редуктор. Этот Редьюсер из фабричной функции Редуктор, фабричная функция принимает тип параметров префикса, а затем возвращает новую функцию Редуктора.
const initialPaginationState = {
startElement: 0,
pageSize: 100,
count: 0,
};
const paginationReducerFor = (prefix) => {
const paginationReducer = (state = initialPaginationState, action) => {
const { type, payload } = action;
switch (type) {
case prefix + types.SET_PAGINATION:
const {
startElement,
pageSize,
count,
} = payload;
return Object.assign({}, state, {
startElement,
pageSize,
count,
});
default:
return state;
}
};
return paginationReducer;
};
// example usages
const usersReducer = combineReducers({
usersData: usersDataReducer,
paginationData: paginationReducerFor('USERS_'),
});
const domainsReducer = combineReducers({
domainsData: domainsDataReducer,
paginationData: paginationReducerFor('DOMAINS_'),
});
заводская функция редуктораpaginationReducerFor
Получает параметр префикса типа, который будет добавляться ко всем типам в функции редуктора. Фабрика возвращает новый редуктор со всеми префиксами типов. Теперь при запуске программы типаUSERS_SET_PAGINATION
действие, это вызовет только обновление редуктора перелистывания страниц информации о пользователе. Редуктор перелистывания страниц для доменного имени остается прежним. Это эффективно повторно использует функции редуктора в нескольких местах в хранилище. Для полноты кода здесь также есть фабричная функция создателя действия с префиксом:
const setPaginationFor = (prefix) => {
const setPagination = (response) => {
const {
startElement,
pageSize,
count,
} = response;
return {
type: prefix + types.SET_PAGINATION,
payload: {
startElement,
pageSize,
count,
},
};
};
return setPagination;
};
// example usages
const setUsersPagination = setPaginationFor('USERS_');
const setDomainsPagination = setPaginationFor('DOMAINS_');
5. Интегрируйте React
Есть некоторые приложения Redux, которым никогда не нужно отображать представление для пользователя (например, интерфейс), но в большинстве случаев вам нужно представление для отображения данных. На данный момент самой популярной библиотекой пользовательского интерфейса для работы с Redux является React. Это библиотека пользовательского интерфейса, которая будет использоваться далее, чтобы показать, как интегрироваться с Redux. Мы можем использовать стратегии, изученные в предыдущих разделах, чтобы сделать код представления более дружественным. Для интеграции будем использоватьreact-redux
библиотека классов
Полезный режим интеграции пользовательского интерфейса является использование данных в доступ для доступа к данным в представлении.react-redux
Удобным местом для размещения аксессоров являетсяmapStateToProps
в функции. Эта функция находится вconnect
Функция (функция, используемая для подключения компонента React к хранилищу Redux) передается при вызове. Здесь вы можете сопоставить данные в состоянии со свойствами, полученными компонентом. Это идеальное место для использования селекторов для получения данных из состояния и передачи их компоненту в качестве свойства. Пример интеграции выглядит следующим образом:
const ConnectedComponent = connect(
(state) => {
return {
users: selectors.getCurrentUsers(state),
editingUser: selectors.getEditingUser(state),
... // other props from state go here
};
}),
mapDispatchToProps // another `connect` function
)(UsersComponent);
Эта интеграция между React и Redux также дает нам место для инкапсуляции действий с использованием областей видимости и типов. Нам нужно, чтобы обработчик компонента мог вызывать хранилище и вызывать создателя действия. Для выполнения этой задачи вreact-redux
вызыватьconnect
мы проходим вmapDispatchToProps
функция. функцияmapDispatchToProps
вызывает РедуксbindActionCreators
Функции используются там, где методы действия и отправки связаны вместе. В нем мы можем привязать область действия к действию, как показано в предыдущем разделе. Например, если вы планируете использовать редьюсер с ограниченным режимом для реализации функции перелистывания страниц на странице списка пользователей, код будет следующим:
const ConnectedComponent = connect(
mapStateToProps,
(dispatch) => {
const actions = {
...actionCreators, // other normal actions
setPagination: actionCreatorFactories.setPaginationFor('USERS_'),
};
return bindActionCreators(actions, dispatch);
}
)(UsersComponent);
теперь изUsersPage
С точки зрения компонента, он получает в качестве свойств список пользователей и другие фрагменты состояния, а также связанный создатель действия. Компоненту не нужно заботиться о том, какие действия с областью действия ему нужны или как получить доступ к состоянию; мы рассмотрели эти проблемы на уровне интеграции. Этот механизм позволяет нам создавать очень слабо связанные компоненты, которые не зависят от внутренней работы состояния. Будем надеяться, что с помощью различных шаблонов, обсуждаемых здесь, мы все сможем создавать приложения Redux масштабируемым, поддерживаемым и разумным способом.
больше ссылок:
- Только что рассмотренная библиотека классов управления состояниемRedux
- используется для создания селекторовReselectбиблиотека классов
- Normalizrэто библиотека для «нормализации» данных JSON. Очень полезно для хранения данных с индексами
- Библиотека промежуточного программного обеспечения для использования асинхронных действий в ReduxRedux-Thunk
- Еще одна промежуточная библиотека для асинхронных действий с использованием генераторов ES2016.Redux-Saga
Эта статья также была опубликована в моемЗнайте переднюю колонку, приветствую всех, чтобы обратить внимание