React Redux - могу ли я сделать mapStateToProps принимать только часть состояния?

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

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

Мне бы хотелось, чтобы mapStateToProps принимал только тот "срез состояния", который я обрабатываю в своем модуле (например, редуктор). Таким образом, мой модуль будет действительно модульным. Это возможно? Думаю, я мог бы просто передать этот срез состояния, чтобы быть реквизитом этого компонента (так что я мог бы просто использовать второй аргумент mapStateToProps, ownProps), но не уверен, что это будет иметь тот же эффект.

Ответ 1

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

Конкретный ответ на ваш вопрос: "нет, mapState всегда дается полное дерево состояний".

У меня есть ссылки на ряд релевантных ресурсов, которые могут помочь в вашей ситуации:

  • Существует несколько существующих библиотек, которые пытаются реализовать "состояние для компонентов в Redux". У меня есть список из них в моем каталоге дополнений Redux, в категории Component State.
  • Группа разработчиков обсуждает и прототипирует различные подходы к концепции "многоразового логического модуля в Redux". Их работа находится в https://github.com/slorber/scalable-frontend-with-elm-or-redux.
  • Недавно Рэнди Кулман опубликовал серию блога из трех частей, посвященную инкапсуляции и модульности в Redux. Он не придумал окончательных ответов, но сообщения заслуживают внимания: Инкапсуляция дерева дерева Redux, Redux Reducer Asymmetry и Модульные редукторы и селектор.

Ответ 2

В Redux есть только один магазин, как вам известно, поэтому все, что он знает, - это передать весь магазин в функцию mapStateToProps. Однако, используя деструктурирование объектов, вы можете указать, какие свойства в нужном вами хранилище и игнорировать остальные. Что-то вроде "function mapStateToProps ({prop1, prop2})" будет захватывать только эти два свойства в хранилище и игнорировать остальные. Ваша функция все еще получает весь магазин, но вы указываете, что вас интересуют только эти реквизиты.

В моем примере "prop1" и "prop2" будут именами, которые вы назначили своим редукторам во время вызова "combReducers".

Ответ 3

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

Например: -

function mapStateToProps(state){
    const { auth } = state  //just taking a auth as example.
    return{
      auth
    }
}

Ответ 4

Я столкнулся с той же проблемой, потому что, как вы сказали, текущая реализация redux/react-redux позволяет разделить редукторы на состояние просто отлично, но mapDispatchToProps всегда передает все дерево состояний.

fooobar.com/questions/841878/... - это не то, что я хочу, потому что это означает, что мы должны дублировать всю нашу селекторную логику в каждом приложении response-redux, использующем наш модуль.

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

Пример:

Как правило, вы хотите сделать это:

const mapStateToProps = (state) => {
  return {
    items: mySelector(state)
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    doStuff: (item) => {
      dispatch(doStuff(item))
    }
  }
}

class ModularComponent extends React.Component {
  render() {
    return (
      <div>
        { this.props.items.map((item) => {
          <h1 onclick={ () => this.props.doStuff(item) }>{item.title}</h1>
        })}
      </div>
    )
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(ModularComponent)

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

const mapStateToProps = (_, ownProps) => {
  return {
    items: mySelector(ownProps.items)
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    doStuff: (item) => {
      dispatch(doStuff(item))
    }
  }
}

class ModularComponent extends React.Component {
  render() {
    return (
      <div>
        { this.props.items.map((item) => {
          <h1 onclick={ () => this.props.doStuff(item) }>{item.title}</h1>
        })}
      </div>
    )
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(ModularComponent)

И приложение, использующее модуль, выглядит так:

const mapStateToProps = (state) => {
  return {
    items: state.items
    stuffForAnotherModule: state.otherStuff
  }
}

class Application extends React.Component {
  render() {
    return (
      <div>
        <ModularComponent items={ this.props.items } />
        <OtherComponent stuff={ this.props.stuffForAnotherModule } />
      </div>
    )
  }
}

export default connect(mapStateToProps)(Application)

Ответ 5

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

Итак, скажем, ваше состояние выглядит так:

{
    account: {
        username: "Jane Doe",
        email: "[email protected]",
        password: "12345",
        ....
    },
    someOtherStuff: {
        foo: 'bar',
        foo2: 'bar2'
    },
    yetMoreStuff: {
        usuless: true,
        notNeeded: true
    }
}

а вашему компоненту требуется все: от account и foo от someOtherStuff, тогда ваш mapStateToProps будет выглядеть следующим образом:

const mapStateToProps = ({ account, someOtherStuff }) => ({
    account,
    foo: { someOtherStuff }
});
export default connect(mapStateToProps)(ComponentName)

то ваш компонент будет иметь prop account и foo, отображаемые из вашего состояния редукции.

Ответ 6

У вас есть возможность написать пару утилит-оболочек для ваших модулей, которые будут выполнять следующие действия: 1) запускать mapStateToProps только тогда, когда срез состояния модуля изменяется, и 2) передавать срез модуля только в mapStateToProps.

Все это предполагает, что фрагменты состояния вашего модуля являются корневыми свойствами объекта состояния приложения (например, state.module1, state.module2).

  1. Пользовательская areStatesEqual обертка areStatesEqual, обеспечивающая выполнение mapStateToProps, только если изменяется mapStateToProps модуля:
function areSubstatesEqual(substateName) {
  return function areSubstatesEqual(next, prev) {
    return next[substateName] === prev[substateName];
  };
}

Затем передайте его в connect:

connect(mapStateToProps, mapConnectToProps, null, {
  areStatesEqual: areSubstatesEqual('myModuleSubstateName')
})(MyModuleComponent);
  1. Пользовательская оболочка mapStateToProps которая передается только в подсостояние модуля:
function mapSubstateToProps(substateName, mapStateToProps) {
  var numArgs = mapStateToProps.length;

  if (numArgs !== 1) {
    return function(state, ownProps) {
      return mapStateToProps(state[substateName], ownProps);
    };
  }

  return function(state) {
    return mapStateToProps(state[substateName]);
  };
}

И вы бы использовали это так:

function myComponentMapStateToProps(state) {
  // Transform state
  return props;
}
var mapSubstate = mapSubstateToProps('myModuleSubstateName', myComponentMapStateToProps);

connect(mapSubstate, mapDispatchToState, null, {
  areStatesEqual: areSubstatesEqual('myModuleSubstateName')
})(MyModuleComponent);

Не проверенный, последний пример должен запускать myComponentMapStateToProps только при изменении состояния "myModuleSubstateName", и он будет получать только подсостояние модуля.

Еще одним усовершенствованием может быть написание собственной функции connect основе модулей, которая принимает один дополнительный параметр moduleName:

function moduleConnect(moduleName, mapStateToProps, mapDispatchToProps, mergeProps, options) {
  var _mapState = mapSubstateToProps(moduleName, mapStateToProps);
  var _options = Object.assign({}, options, {
    areStatesEqual: areSubstatesEqual('myModuleSubstateName')
  });
  return connect(_mapState, mapDispatchToProps, mergeProps, _options);
}

Тогда каждый компонент модуля должен был бы просто сделать:

moduleConnect('myModuleName', myMapStateToProps)(MyModuleComponent);

Ответ 7

Ответ на ваш вопрос - да. Оба эти ответы охватывают разные аспекты одной и той же вещи. Во-первых, Redux создает единый магазин с несколькими редукторами. Поэтому вы захотите объединить их так:

export default combineReducers({
  people: peopleReducer,
  departments: departmentsReducer,
  auth: authenticationReducer
});

Затем, скажем, у вас есть компонент DepartmentsList, вам может просто нужно отобразить departments из хранилища в ваш компонент (и, возможно, некоторые действия сопоставлены с реквизитами):

function mapStateToProps(state) {
  return { departments: state.departments.departmentsList };
}

export default connect(mapStateToProps, { fetchDepartments: fetchDepartments })(DepartmentsListComponent);

Тогда внутри вашего компонента это в основном:

this.props.departments
this.props.fetchDepartments()