Redux: объединение селекторов с редукторами

В этом руководстве Redux: Colocating Selector with Reducers Egghead Dan Abramov предлагает использовать селекторы, которые принимают полное дерево состояний, а не кусочки состояний, для инкапсуляции знаний о состоянии вне компонентов. Он утверждает, что это облегчает изменение государственной структуры, поскольку компоненты не знают об этом, с чем я полностью согласен.

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

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

const todos = (state = [], action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, todo(undefined, action)];
    case 'TOGGLE_TODO':
      return state.map(t => todo(t, action));
    default:
      return state;
  }
};

export default todos;

export const getVisibleTodos = (globalState, filter) => {
  switch (filter) {
    case 'all':
      return globalState.todos;
    case 'completed':
      return globalState.todos.filter(t => t.completed);
    case 'active':
      return globalState.todos.filter(t => !t.completed);
    default:
      throw new Error(`Unknown filter: ${filter}.`);
  }
};

Есть ли какой-либо недостаток, чтобы сделать это таким образом?

1 ответ

Сделав эту ошибку самостоятельно (не с Redux, а с аналогичной внутренней платформой Flux), проблема заключается в том, что предлагаемый вами подход связывает селекторы с расположением соответствующего состояния редуктора в общем дереве состояний. Это вызывает проблему в нескольких случаях:

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

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

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

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

// in file rootselectors.js
import * as todoSelectors from 'todos/selectors';
//...
// something like this:
export const todo = shiftSelectors(state => state.todos, todoSelectors); 

(shiftSelectors имеет простую реализацию - я подозреваю, что библиотека повторного выбора уже имеет подходящую функцию).

Это также дает вам интервал имен - все селекторы задач доступны в экспорте 'todo'. Теперь, если у вас есть два списка задач, вы можете легко экспортировать todo1 и todo2 и даже предоставлять доступ к динамическим спискам, экспортируя запомненную функцию, чтобы создать их, скажем, для определенного индекса или идентификатора. (например, если вы можете отображать произвольный набор списков задач одновременно). Например

export const todo = memoize(id => shiftSelectors(state => state.todos[id], todoSelectors)); 
// but be careful if there are lot of ids!

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

export function selectSomeState(todos, user) {...}

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

Таким образом, для небольшого одноразового приложения оно, вероятно, не очень полезно и просто добавляет шаблон (особенно в JavaScript, который не является самым кратким функциональным языком). Для большого набора приложений, использующего много общих компонентов, он позволяет многократно использовать, и он четко определяет обязанности. Это также упрощает селекторы на уровне модулей, поскольку им не нужно спускаться до соответствующего уровня. Кроме того, если вы добавите FlowType или TypeScript, вы избежите действительно серьезной проблемы того, что все ваши подмодули должны зависеть от типа корневого состояния (в основном, упомянутая мной неявная зависимость становится явной).

Другие вопросы по тегам