6 Comments

Redux: Re-Rendering Caused by mapDispatchToProps

I’ve worked on a couple of React/Redux projects now, and it was only recently that I realized some of the re-rendering issues I’ve run into were being caused by incorrectly using connect, and specifically the second argument to connectmapDispatchToProps.

In this post, I’ll point out the mistakes I’ve been making with connect and mapDispatchToProps, and how I’ve changed my usage to reduce unecessary re-renders.

Example Usage

Like many other developers, I looked to the Redux Documentation to get up to speed on working with Redux. Specifically, I relied on the Example: Todo List section for examples I could start with and adapt for my own purposes.

Here’s the example code for the containers/FilterLink.js file:

  
import { connect } from 'react-redux'
import { setVisibilityFilter } from '../actions'
import Link from '../components/Link'

const mapStateToProps = (state, ownProps) => ({
  active: ownProps.filter === state.visibilityFilter
})

const mapDispatchToProps = (dispatch, ownProps) => ({
  onClick: () => dispatch(setVisibilityFilter(ownProps.filter))
})

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Link)
  

As you can see, the mapDispatchToProps takes two arguments: the dispatch function (coming from Redux) and the props being passed down into this container called ownProps.

What I didn’t realize for a long time is that this code will result in a re-render every time the component receives new props—regardless of whether or not anything this component cares about changed in those props.

The mapDispatchToProps Function

The Redux API docs go into quite a bit of detail about the various ways to call connect. In describing the second argument (mapDispatchToProps), it states that it can either be an Object or a Function. And if it’s a Function (as it is in the Todo example above), it states:

If your mapDispatchToProps function is declared as taking two parameters, it will be called with dispatch as the first parameter and the props passed to the connected component as the second parameter, and will be re-invoked whenever the connected component receives new props. (The second parameter is normally referred to as ownProps by convention.)

What I’d apparently glossed over when trying to learn about connect and mapDispatchToProps was the “… and will be re-invoked whenever the connected component receives new props” part.

Just the presence of the second parameter in the function declaration is enough to cause the mapDispatchToProps function to be invoked every time the props change–even if you don’t use the props at all in your function! (Lint rules that prevent unused variables can help you avoid this, but it’s still good to understand.)

Breaking Shallow Equal

The Todo example above appears to illustrate a legitimate use case for using ownProps in a mapDispatchToProps function. But by implementing it this way, the component will now re-render every time it receives new props.

This is because every time the mapDispatchToProps function is called, it returns an object with a brand new lambda for onClick that closes over the current ownProps.filter value. That means the resulting object will never be “shallow equal” to the prior result of mapDispatchToProps (where shallow equal means that all properties of two objects are === to each other, but the top-level objects themselves aren’t necessarily ===).

Don’t Use ownProps in mapDispatchToProps

To avoid running into this issue, I’m trying to avoid using the ownProps parameter in my mapDispatchToProps functions. While it might be slightly more convenient to have your dispatch functions close over their needed props right in the mapDispatchToProps, it can be difficult to identify, and later fix, any re-render issues that this causes–especially when the component ends up wrapping components that are expensive to render.

Here’s how I’d re-write the example mapDispatchToProps from the example Todo code:

  
const mapStateToProps = (state, ownProps) => ({
  active: ownProps.filter === state.visibilityFilter
})

const mapDispatchToProps = (dispatch) => ({
  handleClick: (filter) => dispatch(setVisibilityFilter(filter))
})

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Link)
  

The mapDispatchToProps no longer expects the ownProps parameter, which means the filter now needs to be passed in to the renamed handleClick function. This change requires an update to the presentation component, as well:

  
class Link extends React.PureComponent {

  onClick = () => {
    this.props.handleClick(this.props.filter);
  }

  render() {
    const { active, children } = this.props;
    return (
      <button
        onClick={this.onClick}
        disabled={active}
        style={{
            marginLeft: '4px',
        }}
      >
        {children}
      </button>
    );
  }
}
  

The caller of the handleClick function is now passing in the filter value from props. If you look at the original code for this component (components/Link.js), you’ll also note that I’ve converted it from a function to a class. I did this so I could make an onClick method that has access to props. This means that the exact same function (===) will be used in the JSX every time this component is rendered.

Shorthand Notation

A great way to enforce the rule that you never use ownProps in your mapDispatchToProps functions is to not write mapDispatchToProps functions at all! Instead, you can just pass in an object as the second argument to connect as described in the Redux API docs:

If an object is passed, each function inside it is assumed to be a Redux action creator. An object with the same function names, but with every action creator wrapped into a dispatch call so they may be invoked directly, will be merged into the component’s props.

That means we could update the example one last time as follows:

  
const mapStateToProps = (state, ownProps) => ({
  active: ownProps.filter === state.visibilityFilter
})

const mapDispatchToProps = {
  handleClick: setVisibilityFilter
};

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Link)
  

Learning to pass an object instead of a function in these situations has helped me avoid unexpected re-renders. What other tricks have you found?