简体   繁体   中英

React Redux — can I make mapStateToProps only take in part of the state?

I want to make reusable modules that could be plugged in to any react-redux application. Ideally, my module would have a container component, actions, and reducer at the top level (and then any presentational components below the container). I would want the module to only work off its own slice of the app's state, and ideally to not have to know anything about the rest of the app state (so it's truly modular).

Reducers only work off of part of the state (using combineReducers), so I'm happy there. However, with container components, it seems like mapStateToProps always takes in the full state of the app.

I'd like it if mapStateToProps only took in the same "state slice" that I am handling in my module (like the reducer does). That way my module would truly be modular. Is this possible? I guess I could just pass that slice of the state down to be the props of this component (so I could just use the second argument of mapStateToProps, ownProps), but am not sure if this would have the same effect.

That is actually something of a complicated topic. Because Redux is a single global store, the idea of a completely encapsulated, fully reusable plug-and-play set of logic does become rather difficult. In particular, while the reducer logic can be fairly generic and ignorant of where it lives, the selector functions need to know where in the tree to find that data.

The specific answer to your question is "no, mapState is always given the complete state tree".

I do have links to a number of relevant resources, which may possibly help with your situation:

Redux only has a single store as you know, so all it knows to do is pass the entire store to your mapStateToProps function. However using object destructuring, you can specify which properties in the store you want and ignore the rest. Something like 'function mapStateToProps({prop1, prop2})' would only capture those two properties in the store and ignore the rest. Your function is still receiving the entire store, but you're indicating that only these props interest you.

In my example, 'prop1' and 'prop2' would be the names you assigned your reducers during the call to 'combineReducers'.

Ideally the way it works is you get the state and you extract the values from them by use deconstructors. redux works on concept of single state

For example:-

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

I'm running into the same problem because, as you said, the current implementation of redux/react-redux allows for splitting up reducers on the state just fine but mapDispatchToProps always passes the whole state tree.

https://stackoverflow.com/a/39757853/444794 is not what I want, because it means we have to duplicate all our selector logic across each react-redux application that uses our module.

My current workaround has been to pass the slice of the state down as a prop instead. This follows a sort of compositional pattern but at the same time removes the cleanliness of accessing the state directly, which I'm disappointed with.

Example:

Generally, you want to do this:

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)

but since this module is included in an application where the state is now several things (ie. key-values) rather than a list of items, this won't work. My workaround instead looks like:

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)

And the application using the Module looks like:

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)

Although mapStateToProps (the first function you pass to connect) gets passed the whole store as you said, its job is to map specific parts of the state to the component. So only what is returned from mapStateToProps will be mapped as a prop to your component.

So lets say your state looks like this:

{
    account: {
        username: "Jane Doe",
        email: "janedoe@somemail.com",
        password: "12345",
        ....
    },
    someOtherStuff: {
        foo: 'bar',
        foo2: 'bar2'
    },
    yetMoreStuff: {
        usuless: true,
        notNeeded: true
    }
}

and your component needs everything from account and foo from someOtherStuff then your mapStateToProps would look like this:

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

then your component will have the prop account and foo mapped from your redux state.

You do have the option of writing a couple of wrapper utils for your modules that will do the work of: 1) Only running mapStateToProps when the module's slice of state changes and 2) only passes in the module's slice into mapStateToProps.

This all assumes your module slices of state are root properties on the app state object (eg state.module1 , state.module2 ).

  1. Custom areStatesEqual wrapper function that ensures mapStateToProps will only run if the module's sub-state changes:
function areSubstatesEqual(substateName) {
  return function areSubstatesEqual(next, prev) {
    return next[substateName] === prev[substateName];
  };
}

Then pass it into connect :

connect(mapStateToProps, mapConnectToProps, null, {
  areStatesEqual: areSubstatesEqual('myModuleSubstateName')
})(MyModuleComponent);
  1. Custom mapStateToProps wrapper that only passes in the module substate:
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]);
  };
}

And you'd use it like so:

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

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

While untested, that last example should only run myComponentMapStateToProps when 'myModuleSubstateName' state changes, and it will only receive the module substate.

One additional enhancement could be to write your own module-based connect function that takes one additional moduleName param:

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);
}

Then each module component would just need to do:

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

The answer to your question is yes. Both given answers cover different aspects of the same thing. First, Redux creates a single store with multiple reducers. So you'll want to combine them like so:

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

Then, say you have a DepartmentsList component, you may just need to map the departments from the store to your component (and maybe some actions mapped to props as well):

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

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

Then inside your component it is basically:

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM