Перейти к основному содержимому

Структура кода

Неофициальный Бета-перевод

Эта страница переведена 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:

  • /src
    • index.tsx: Entry point file that renders the React component tree
    • /app
      • store.ts: store setup
      • rootReducer.ts: root reducer (optional)
      • App.tsx: root React component
    • /common: hooks, generic components, utils, etc
    • /features: contains all "feature folders"
      • /todos: a single feature folder
        • todosSlice.ts: Redux reducer logic and associated actions
        • Todos.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"). Кроме того, при переносе логики в редюсеры больше функциональности будет доступно для отладки с помощью "машины времени".

Следующий комментарий хорошо резюмирует дихотомию:

Проблема заключается в том, что помещать в создателя действий, а что — в редюсер, выбирая между "тяжелыми" и "легкими" объектами действий. Если вся логика находится в создателе действий, вы получаете "тяжелые" объекты действий, которые по сути объявляют обновления состояния. Редюсеры становятся чистыми, простыми функциями добавления, удаления и обновления. Их будет легко компоновать, но в них не останется бизнес-логики. Если же перенести больше логики в редюсеры, вы получите компактные объекты действий и централизованную логику работы с данными, но редюсеры станет сложнее компоновать, поскольку им может потребоваться информация из других веток состояния. В итоге вы получите крупные редюсеры или редюсеры, принимающие дополнительные аргументы из вышестоящих частей состояния.

Совет

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

Дополнительные материалы

Документация

Статьи

Обсуждения

Зачем использовать создатели действий?

Redux не требует использования создателей действий. Вы свободны в создании действий любым удобным способом, включая передачу литерала объекта в dispatch. Создатели действий появились из архитектуры Flux и были приняты сообществом Redux благодаря нескольким преимуществам:

Создатели действий удобнее поддерживать. Обновления действия выполняются в одном месте и применяются везде. Все экземпляры действия гарантированно имеют одинаковую структуру и значения по умолчанию.

Создатели действий проще тестировать. Корректность встроенного действия требует ручной проверки. Как и любая функция, создатель действий тестируется один раз и запускается автоматически.

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

Создатели действий - более мощная абстракция. Создание действия часто включает преобразование данных или AJAX-запросы. Создатели действий предоставляют единообразный интерфейс для этой разнородной логики. Эта абстракция освобождает компонент от деталей создания действия при диспетчеризации.

Дополнительные материалы

Статьи

Обсуждения

Где должны находиться вебсокеты и другие постоянные подключения?

Мидлвары — это правильное место для постоянных соединений, таких как вебсокеты, в Redux-приложении по нескольким причинам:

  • Мидлвары существуют на протяжении всего жизненного цикла приложения

  • Как и со стором, вам, вероятно, нужен только один экземпляр соединения, который будет использоваться всем приложением

  • Мидлвары видят все диспатченные экшены и могут диспатчить экшены сами. Это позволяет мидлваре преобразовывать диспатченные экшены в сообщения, отправляемые через вебсокет, и диспатчить новые экшены при получении сообщения.

  • Экземпляр соединения вебсокета не сериализуем, поэтому ему не место в состоянии стора

См. пример, демонстрирующий как мидлвара для сокетов может диспатчить и реагировать на Redux-экшены.

Существует множество готовых мидлвар для вебсокетов и подобных соединений — см. ссылку ниже.

Библиотеки

Как использовать Redux-стор вне компонентов?

В приложении должен быть только один Redux-стор. По сути, он становится архитектурным синглтоном. При использовании с React стор внедряется в компоненты через рендеринг <Provider store={store}> вокруг корневого компонента <App>, поэтому только логика инициализации приложения должна импортировать стор напрямую.

Однако иногда другим частям кода тоже нужно взаимодействовать со стором.

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

Возможные решения:

  • Реализуйте логику, зависящую от стора, как санк (thunk), и диспатчьте его из компонента

  • Передавайте ссылку на dispatch из компонентов как аргумент в соответствующие функции

  • Реализуйте логику в виде мидлвары и добавьте её в стор при инициализации

  • Внедряйте экземпляр стора в нужные файлы при создании приложения

Распространённый кейс — чтение авторизационных данных (например, токена) из состояния Redux внутри интерсептора Axios. Интерсептору нужен доступ к store.getState(), но его импорт в файлы API-слоя создаёт циклические зависимости.

Вместо этого можно экспортировать из файла интерсептора функцию injectStore:

common/api.js
let store

export const injectStore = _store => {
store = _store
}

axiosInstance.interceptors.request.use(config => {
config.headers.authorization = store.getState().auth.token
return config
})

Затем в точке входа приложения внедрите стор в настройку API:

index.js
import store from './app/store'
import { injectStore } from './common/api'
injectStore(store)

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