Redux: селектор Colocating с редукторами

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

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

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

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. Теперь, если у вас есть два списка todo, вы можете легко экспортировать todo1 и todo2 и даже предоставлять доступ к динамическим, экспортируя memoized функцию для их создания для определенного индекса или идентификатора, скажем. (например, если вы можете отображать произвольный набор списков todo за раз). Например.

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) {...}

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

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