Структура кода
Эта страница переведена PageTurner AI (бета). Не одобрена официально проектом. Нашли ошибку? Сообщить о проблеме →
Redux FAQ: Структура кода
Как должна выглядеть структура файлов? Как группировать action creators и редюсеры? Где размещать селекторы?
Поскольку Redux — это просто библиотека для хранения данных, она не навязывает определённую структуру проекта. Однако существуют распространённые подходы, которые используют большинство разработчиков:
-
Rails-style: отдельные папки для "actions", "constants", "reducers", "containers" и "components"
-
"Feature folders" / "Domain"-style: отдельные папки для каждой фичи или домена, иногда с подпапками по типам файлов
-
"Ducks/Slices": похоже на domain style, но с явной связью экшенов и редюсеров, часто через их определение в одном файле
Обычно рекомендуется определять селекторы рядом с редюсерами и экспортировать их для переиспользования (например, в mapStateToProps, асинхронных action creators или сагах). Это позволяет сосредоточить в редюсерах весь код, который знает о структуре состояния.
Мы особенно рекомендуем организовывать логику в "фичевые папки" (feature folders), размещая всю Redux-логику для конкретной фичи в одном файле "slice/ducks".
Пример структуры:
Detailed Explanation: Example Folder Structure
An example folder structure might look something like:
/srcindex.tsx: Entry point file that renders the React component tree/appstore.ts: store setuprootReducer.ts: root reducer (optional)App.tsx: root React component
/common: hooks, generic components, utils, etc/features: contains all "feature folders"/todos: a single feature foldertodosSlice.ts: Redux reducer logic and associated actionsTodos.tsx: a React component
/app contains app-wide setup and layout that depends on all the other folders.
/common contains truly generic and reusable utilities and components.
/features has folders that contain all functionality related to a specific feature. In this example, todosSlice.ts is a "duck"-style file that contains a call to RTK's createSlice() function, and exports the slice reducer and action creators.
Хотя организация файлов на диске может быть любой, важно помнить: экшены и редюсеры не должны рассматриваться изолированно. Редюсер из одной папки может (и должен) реагировать на экшен из другой папки.
Дополнительные материалы
Документация
Статьи
Обсуждения
Как распределить логику между редюсерами и создателями действий? Где должна находиться "бизнес-логика"?
Не существует однозначного ответа на вопрос, какие именно части логики должны находиться в редюсере, а какие — в создателе действий. Некоторые разработчики предпочитают "тяжелых" создателей действий с "легкими" редюсерами, которые просто берут данные из действия и слепо объединяют их с текущим состоянием. Другие стараются минимизировать размер действий и ограничить использование getState() в создателях действий. (В контексте этого вопроса другие асинхронные подходы, такие как саги и наблюдатели, относятся к категории "создателей действий".)
Размещение большей части логики в редюсерах имеет несколько преимуществ. Типы действий становятся более семантичными и осмысленными (например, "USER_UPDATED" вместо "SET_STATE"). Кроме того, при переносе логики в редюсеры больше функциональности будет доступно для отладки с помощью "машины времени".
Следующий комментарий хорошо резюмирует дихотомию:
Проблема заключается в том, что помещать в создателя действий, а что — в редюсер, выбирая между "тяжелыми" и "легкими" объектами действий. Если вся логика находится в создателе действий, вы получаете "тяжелые" объекты действий, которые по сути объявляют обновления состояния. Редюсеры становятся чистыми, простыми функциями добавления, удаления и обновления. Их будет легко компоновать, но в них не останется бизнес-логики. Если же перенести больше логики в редюсеры, вы получите компактные объекты действий и централизованную логику работы с данными, но редюсеры станет сложнее компоновать, поскольку им может потребоваться информация из других веток состояния. В итоге вы получите крупные редюсеры или редюсеры, принимающие дополнительные аргументы из вышестоящих частей состояния.
Мы рекомендуем размещать как можно больше логики в редюсерах. Иногда может потребоваться предварительная обработка данных перед созданием действия, но основную работу должны выполнять именно редюсеры.
Дополнительные материалы
Документация
Статьи
Обсуждения
-
Как размещение логики в создателях действий влияет на отладку
-
#384: Чем больше логики в редукторе, тем больше можно воспроизвести через "машину времени"
-
#1171: Рекомендации по best practices для создателей действий, редукторов и селекторов
-
Stack Overflow: Доступ к состоянию Redux в создателе действий?
Зачем использовать создатели действий?
Redux не требует использования создателей действий. Вы свободны в создании действий любым удобным способом, включая передачу литерала объекта в dispatch. Создатели действий появились из архитектуры Flux и были приняты сообществом Redux благодаря нескольким преимуществам:
Создатели действий удобнее поддерживать. Обновления действия выполняются в одном месте и применяются везде. Все экземпляры действия гарантированно имеют одинаковую структуру и значения по умолчанию.
Создатели действий проще тестировать. Корректность встроенного действия требует ручной проверки. Как и любая функция, создатель действий тестируется один раз и запускается автоматически.
Создатели действий легче документировать. Параметры создателя действий перечисляют зависимости действия. Централизация определения действия предоставляет удобное место для комментариев документации. При встроенном определении действия эту информацию сложнее зафиксировать и передать.
Создатели действий - более мощная абстракция. Создание действия часто включает преобразование данных или AJAX-запросы. Создатели действий предоставляют единообразный интерфейс для этой разнородной логики. Эта абстракция освобождает компонент от деталей создания действия при диспетчеризации.
Дополнительные материалы
Статьи
Обсуждения
Где должны находиться вебсокеты и другие постоянные подключения?
Мидлвары — это правильное место для постоянных соединений, таких как вебсокеты, в Redux-приложении по нескольким причинам:
-
Мидлвары существуют на протяжении всего жизненного цикла приложения
-
Как и со стором, вам, вероятно, нужен только один экземпляр соединения, который будет использоваться всем приложением
-
Мидлвары видят все диспатченные экшены и могут диспатчить экшены сами. Это позволяет мидлваре преобразовывать диспатченные экшены в сообщения, отправляемые через вебсокет, и диспатчить новые экшены при получении сообщения.
-
Экземпляр соединения вебсокета не сериализуем, поэтому ему не место в состоянии стора
См. пример, демонстрирующий как мидлвара для сокетов может диспатчить и реагировать на Redux-экшены.
Существует множество готовых мидлвар для вебсокетов и подобных соединений — см. ссылку ниже.
Библиотеки
Как использовать Redux-стор вне компонентов?
В приложении должен быть только один Redux-стор. По сути, он становится архитектурным синглтоном. При использовании с React стор внедряется в компоненты через рендеринг <Provider store={store}> вокруг корневого компонента <App>, поэтому только логика инициализации приложения должна импортировать стор напрямую.
Однако иногда другим частям кода тоже нужно взаимодействовать со стором.
Старайтесь избегать прямого импорта стора в другие файлы. Хотя это может работать в некоторых случаях, часто приводит к циклическим зависимостям при импорте.
Возможные решения:
-
Реализуйте логику, зависящую от стора, как санк (thunk), и диспатчьте его из компонента
-
Передавайте ссылку на
dispatchиз компонентов как аргумент в соответствующие функции -
Реализуйте логику в виде мидлвары и добавьте её в стор при инициализации
-
Внедряйте экземпляр стора в нужные файлы при создании приложения
Распространённый кейс — чтение авторизационных данных (например, токена) из состояния Redux внутри интерсептора Axios. Интерсептору нужен доступ к store.getState(), но его импорт в файлы API-слоя создаёт циклические зависимости.
Вместо этого можно экспортировать из файла интерсептора функцию injectStore:
let store
export const injectStore = _store => {
store = _store
}
axiosInstance.interceptors.request.use(config => {
config.headers.authorization = store.getState().auth.token
return config
})
Затем в точке входа приложения внедрите стор в настройку API:
import store from './app/store'
import { injectStore } from './common/api'
injectStore(store)
Таким образом, только код инициализации приложения импортирует стор, а граф зависимостей файлов избегает циклов.