This document details some tips and tricks for creating redux containers. Specifically, this document is looking at the mapDispatchToProps argument of the connect function from react-redux. There are many ways to write the same thing in redux. This gist covers the various forms that mapDispatchToProps can take.
Before we dig too deep into how all of this works, it's a good idea to look at what we're trying to create. Here we can see an example of a mapDispatchToProps argument that uses every feature. This example highlights the key advantages of mixing the functional long-hand version of mapDispatchToProps with bindActionCreators and thunks.
Enables:
- access to
ownProps - access to
getState - controlling the
event - controlling dispatch
- conditional dispatches
- multiple dispatches
If you don't fully understand what these examples are doing, don't worry. We'll cover all of these pieces in great detail below. We're hoisting this to the top of this doc to make them accessible.
Key idea: Auto-dispatch a thunk.
Good for when you need access to getState. You don't normally need access to the redux state when you are dispatching. However, there are times when you need to know if something is (for instance) already fetching before trying to fetch it again.
Using bindActionCreators manually gives us access to both the short and long-hand syntaxes simultaneously.
import { bindActionCreators } from 'redux'
import { honk, kill } from '../modules/goose/actions'
import { selectAlive } from '../modules/goose/selectors'
const mapDispatchToProps = (dispatch, ownProps) => bindActionCreators({
onClick: (event) => (_, getState) => {
event.preventDefault() // <-- control the event
const state = getState() // <-- access the state
const { id } = ownProps // <-- access props
const isAlive = selectAlive(state, id)
if (isAlive) { // <-- conditionally dispatch
dispatch(honk(id))
}
},
onClose: kill // <-- use short-hand if you want
}, dispatch)Note: if you have no need to read from ownProps or state, you might prefer to use one of the simpler versions below.
If you don't like the use of bindActionCreators above, you can accomplish the same thing using the long-hand version.
import { honk, kill } from '../modules/goose/actions'
import { selectAlive } from '../modules/goose/selectors'
const mapDispatchToProps = (dispatch, ownProps) => ({
onClick: (event) => dispatch((_, getState) => { // <-- dispatch a thunk
event.preventDefault()
const state = getState()
const { id } = ownProps
const isAlive = selectAlive(state, id)
if (isAlive) {
dispatch(honk(id))
}
}),
onClose: (...args) => dispatch(kill(...args)) // <-- long-hand version of short-hand
})Of course, if you don't need access to getState or ownProps, you could get away with something more bare-bones. We'll see below how anything less than what's shown below starts to introduce some drawbacks.
import { honk } from '../modules/goose/actions'
// without ownProps
const mapDispatchToProps = {
onClick: () => honk() // <-- rename to event; control the payload
}
// with ownProps
const mapDispatchToProps = (dispatch, ownProps) => ({
onClick: () => {
const { id } = ownProps
dispatch(honk(id)) // <-- control the dispatch
}
})You can read more about why connect exists in the react-redux docs. At a high level, you use connect to create redux containers. In practical terms, a redux container lets you hook the props of a react component to a redux store. If you are new to redux and are unsure what it does, you should start from the beginning.
The connect function accepts four arguments.
mapStateToProps— selects values from the state; creates astatePropsobject.mapDispatchToProps— dispatches actions; creates adispatchPropsobject.mergeProps— not commonly used. Merges the property objects fromstateProps,dispatchPropsandownProps.options— not commonly used. Options for deeper control over what howconnectfunction operates.
Key point: this document is discussing the second argument, mapDispatchToProps.
In effect, the connect function allows you to create redux containers — imagine if it were named createContainer instead. In a react-redux application, a container is a special type of component that has access to the redux store. The arguments passed to connect — mapStateToProps and mapDispatchToProps — are used to configure how the container communicate with the store. Both arguments are designed to gather props from the store and pass them to the child component.
A container is created by wrapping a child component in connect. In all of the examples below, imagine that our mapDispatchToProps argument will be creating props for a <SomeButton /> component to use.
The redux store has two key functions that are of interest: getState and dispatch. The first argument, mapStateToProps, is focused on the getState function. The point of the function is to return a stateProps object — essentially, a props object with values derived from the state.
The second argument, mapDispatchToProps is focused on the dispatch function. The point is to generate a dispatchProps object. The result is a props object that contains action dispatchers — functions that automatically dispatch actions with the correct payload.
Here you can see the container we'll be using in all of the examples below. Right now we're leaving both of mapStateToProps and mapDispatchToProps as undefined. We'll explore numerous examples of how to craft a mapDispatchToProps argument.
import { connect } from 'react-redux'
import { honk } from '../modules/goose/actions' // <-- action creator
import SomeButton from './SomeButton'
const mapStateToProps = undefined
const mapDispatchToProps = undefined // <-- we're focusing on this one
// hook the props of SomeButton to the redux store
const SomeButtonContainer = connect( // <-- create a container
mapStateToProps,
mapDispatchToProps
)(SomeButton) // <-- child component
export default SomeButtonContainerHere's what the <SomeButton /> component looks like as well.
import React from 'react'
import PropTypes from 'prop-types'
const SomeButton = ({ children, onClick }) => (
<button onClick={onClick}>
{children}
</button>
)
SomeButton.propTypes = {
children: PropTypes.node,
onClick: PropTypes.func
}
export default SomeButtonYou might enjoy reading more about action creators in the redux manual.
Below we see the action creator that we will use for all of the examples below. For simplicity, we're showing the constant in the same file as our action creator. Typically, your action type constants should be kept in a separate location.
Imagine this file is located in a redux module at src/modules/goose/actions/index.js.
export const GOOSE_HONK = 'GOOSE_HONK' // <-- action type
export const GOOSE_KILL = 'GOOSE_KILL'
export const honk = (payload) => ({ type: GOOSE_HONK, payload }) // <-- action creator
export const kill = (payload) => ({ type: GOOSE_KILL, payload })Note: your action type constants should be kept in a separate file.
Specifically, mapDispatchToProps is the second argument that connect expects to receive. In the context of a react-redux application, the mapDispatchToProps argument is responsible for enabling a component to dispatch actions. In practical terms, mapDispatchToProps is where react events (and lifecycle events) are mapped to redux actions.
More specifically, mapDispatchToProps is where you should be dispatching most of your actions. The vast majority of actions in your react-redux application will originate from a mapDispatchToProps argument one way or the other. Any action originating from react, started in a dispatchProps object, created by a mapDispatchToProps argument.
These dispatchProps are all functions that dispatch actions. In the example react component above, the onClick function is assigned to an action dispatcher by the container.
The react-redux API docs for connect mentions three different ways to specify the mapDispatchToProps argument. In all three forms, the point is to generate a dispatchProps object.
- Object short-hand — a key-value object of redux action creators. In the short-hand version, the actions are automatically dispatched using
bindActionCreators. - Functional long-hand — a function that returns a key-value object of redux action creators. In the long-hand version, the actions are not auto-dispatched.
- Factory function — not commonly used. A factory function that returns a
mapDispatchToPropsfunction. Allows for manually memoizing thedispatchPropsobject.
Key point: The purpose of mapDispatchToProps is to create a dispatchProps object.
Technically speaking, a dispatchProps object is a key-mapping of action dispatchers. More specifically, dispatchProps are merged into ownProps by a redux container and passed as merged props to the child component. Generally, the dispatchProps object is what allows a component to dispatch actions to a redux store.
- A
dispatchPropsobject is a key-mapping of action dispatchers. - An action dispatcher is a function that automatically dispatches one or more actions
- A thunk would be considered an action dispatcher.
Key point: The functions in a dispatchProps object are action dispatchers.
It may be helpful for to explore a toy example that demonstrates the core concepts of a dispatchProps object. The linked example shows how a dispatchProps object is used. Typically, a dispatchProps object is created and cached inside the container when connect is initialized.
Play: check out this standalone example: https://repl.it/@heygrady/dispatchProps
As noted above, the connect function specifies three ways to define your mapDispatchToProps argument.
import { honk } from '../modules/goose/actions' // <-- action creator
// object short-hand version
const mapDispatchToProps = { onClick: honk } // <-- auto-dispatches
// functional long-hand version
const mapDispatchToProps = dispatch => ({
onClick: event => dispatch(honk(event)) // <-- manually dispatches
})
// avoid: factory version (usually unnecessary)
const mapDispatchToProps = () => {
let dispatchProps // <-- manually memoizes dispatchProps
return dispatch => {
if (!dispatchProps) { // <-- manually skips rebinding when ownProps changes
dispatchProps = {
onClick: event => dispatch(honk(event))
}
}
return dispatchProps
}
}Note: the factory version is only useful when ownProps is specified (see below).
The connect function is heavily overloaded to enable many common-sense performance optimizations out of the box.
The most obvious examples of overloading connect are the three ways to specify the mapDispatchToProps argument: short-hand, long-hand and factory.
A less obvious optimization applies only to the long-hand form of mapDispatchToProps. The long-hand form is overloaded too! Under the hood, connect will check the argument length of your mapDispatchToProps function and handle it differently.
If you specify only the dispatch argument, the connect function will automatically memoize the result. It will only ever call your mapDispatchToProps function once! This has a great performance benefit in the case that your dispatchProps object is expensive to create. In any case, this memoization ensures that the long-hand version is just as performant as the short-hand object version.
If you specify the optional second ownProps argument, your mapDispatchToProps function will be called whenever props are updated. In cases where your dispatchProps object is expensive to create, this can be a minor performance issue. In very extreme cases you may benefit from the factory version if you need to use ownProps. However, you usually won't see any performance impact from including ownProps.
- Ignore
ownPropsversion — only called whenconnectinitializes - Bind
ownPropsversion — called wheneverownPropschanges
Here we see the two overloaded forms for a mapDispatchToProps function.
import { honk } from '../modules/goose/actions'
// ignore `ownProps`; binds only on init
const mapDispatchToProps = dispatch => ({
onClick: event => dispatch(honk()) // <-- empty payload
})
// bind `ownProps`; binds every time ownProps changes
const mapDispatchToProps = (dispatch, ownProps) => ({
onClick: event => dispatch(honk(ownProps.id)) // <-- bind id to payload
})
// avoid: wasteful; rebinds every time ownProps changes
const mapDispatchToProps = (dispatch, ownProps) => ({
onClick: event => dispatch(honk()) // <-- whoops! we're not using ownProps
})Key point: you should not put ownProps in the signature of your mapDispatchToProps function unless you intend to use it.
It's important to keep in mind that the maintainers of react-redux have gone to great lengths to ensure good performance for most use cases. There are edge cases where you might want to do something special that connect doesn't handle out of the box. If you are using the factory version of mapDispatchToProps you are probably doing something very special.
Most of the time you would have no reason to manually memoize the dispatchProps object.
- If you use the short-hand version,
dispatchPropsis already memoized - If you use the long-hand version,
dispatchPropsis also already memoizedmapDispatchToProps(dispatch)will only be called once, whenconnectis initializedmapDispatchToProps(dispatch, ownProps)will be called wheneverownPropschanges
- If you use the factory version, you will still be doing work to determine if you need to rebind your action creators
- Only useful when binding
ownProps - The inner
dispatchPropscreator will be called every timeownPropschanges
- Only useful when binding
Internally, connect determines if you're using the factory pattern based on what your mapDispatchToProps function returns when it is initialized. If you return a function instead of an object, it's assumed you're trying to specify a factory.
The only time that the factory version will yield performance benefits is in the case where ownProps updates frequently, yet only specific props are bound to your action creators. Say you are binding an id that never changes, but the name prop changes 60 times a second. In that case, you might be able to save some CPU cycles using the factory method.
To see benefits, your mapDispatchToProps factory:
- must access
ownProps - must have an expensive-to-create
dispatchPropsobject - must have noisy values in
ownPropsthat are irrelevant to yourdispatchPropsobject
Note: the factory version still does work every time ownProps changes. If your mapDispatchToProps isn't very complicated, there likely isn't any performance gain to using the factory version versus the functional version. However, if some unrelated values of ownProps are updating constantly (multiple times a second), the factory version can enable you to rebind to ownProps only when props you care about have changed.
Below you can see that the functional version, as opposed to the factory version, will rebind the id to honk on init and every time ownProps changes. If you were to change an unrelated prop, like children, the functional version would still rebind. By contrast, the factory version would only rebind the action creator if the ownProps.id were to change. Otherwise, changes to ownProps will not cause a rebind.
import { honk } from '../modules/goose/actions'
// functional version: might rebind too much
const mapDispatchToProps = (dispatch, ownProps) => { // <-- called on init and when ownProps changes
const { id } = ownProps
return {
onClick: event => dispatch(honk(id)) // <-- bind to id
}
}
// helper functions to manage cache and shallow compare
const filterProps = (props, comparePropNames = []) => comparePropNames.reduce((newProps, prop) => {
newProps[prop] = props[prop]
return newProps
}, {})
const shouldFactoryBindProps = (prevProps, nextProps, comparePropNames = []) => {
if (prevProps === undefined || (prevProps !== undefined && nextProps === undefined)) { return true }
if (prevProps === undefined && nextProps === undefined) { return false }
return comparePropNames.some(prop => prevProps[prop] !== nextProps[prop])
}
// factory version: rebinds only when absolutely necessary
const mapDispatchToPropsFactory = () => { // <-- only called on init
let prevOwnProps
let dispatchProps
const comparePropNames = ['id']
return (dispatch, ownProps) => { // <-- called on init and when ownProps changes
const shouldBind = shouldFactoryBindProps(prevOwnProps, ownProps, comparePropNames)
if (shouldBind) { // <-- skips rebinding
dispatchProps = mapDispatchToProps(dispatch, ownProps) // <-- notice, reusing mapDispatchToProps
prevOwnProps = filterProps(ownProps, comparePropNames)
}
return dispatchProps
}
}
}Note: like the warnings about pure components, it's important to notice that the work required to determine if we should rebind our action creators might not be any faster than simply rebinding.
Play: in the above example, we're actually wrapping our normal mapDispatchToProps function in a factory. If you want to see a generic factory creator, play with this example: https://repl.it/@heygrady/createActionDispatchers
While most developers are most familiar with the object short-hand version of the mapDispatchToProps argument, it should be avoided. The reasons are very subtle. If you are aware of the limitations of the short-hand version, feel free to use it. However, if you would like to write code that is easy to extend, consider the examples in this section.
import { honk } from '../modules/goose/actions'
// short-hand: notice that it dispatches a react event as the payload
const mapDispatchToProps = { onClick: honk }
// long-hand; this example is the functional equivalent of the short-hand version
const mapDispatchToProps = dispatch => ({
onClick: (event) => dispatch(honk(event)) // <-- whoops! passes react event as payload!
})The great benefit of the object short-hand version is that you can easily jam auto-dispatching actions into a component's props. This is most useful when initially sketching out functionality. Most developers prefer this format.
There are also a few downsides to the object short-hand version.
- Easy to write
- Easy to maximize performance
- Easy to dispatch thunks to access
dispatchandgetState
- Difficult to extend
- Pressure to offload work to components
- No control over payloads
- No access to
ownProps - Easy to accidentally dispatch a react event as the payload
- Many developers neglect to rename actions to make sense to a component
import { honk } from '../modules/goose/actions'
// prefer: rename the action, control payload
const mapDispatchToProps = {
onClick: () => honk() // <-- clear naming within component; ignore event; auto-dispatched
}
// avoid: passing action straight through to component
const mapDispatchToProps = {
honk // <-- unclear naming within the component; dispatches event
}
// avoid: letting the component control the payload
const mapDispatchToProps = {
onClick: honk // <-- dispatches react event as the payload
}
// avoid: auto-dispatching explicit return
const mapDispatchToProps = {
onClick: (event) => { // <-- control payload
event.preventDefault() // <-- manage events
return honk() // <-- awkward syntax; auto-dispatch
}
}
// prefer: use a thunk to manually dispatch
const mapDispatchToProps = {
onClick: (event) => (dispatch) => { // <-- access dispatch
event.preventDefault()
dispatch(honk()) // <-- manual dispatch
}
}
// prefer: use a thunk to getState
const mapDispatchToProps = {
onClick: (event) => (dispatch, getState) => {
event.preventDefault()
const state = getState() // <-- access state
const isAlive = selectAlive(state)
if (isAlive) { // <-- conditional dispatch
dispatch(honk(id))
}
}
}The functional long-hand version gives you more control over how your container dispatches actions. While the syntax is slightly longer, you gain far more control. The long-hand version encourages best practices.
- Access to
dispatch - Access to
ownProps - Easy to extend
- Pressure to rename actions
- Pressure to control payloads
- Manually dispatch
- Hidden tricks can impact performance
import { honk } from '../modules/goose/actions'
// prefer: ignore ownProps
const mapDispatchToProps = (dispatch) => ({ // ignore ownProps
onClick: (event) => {
event.preventDefault() // <-- manage events
dispatch(honk()) // <-- manually dispatch
}
})
// prefer: bind ownProps
const mapDispatchToProps = (dispatch, ownProps) => ({ // access ownProps
onClick: (event) => {
event.preventDefault()
const { id } = ownProps
dispatch(honk(id)) // <-- bind ownProps
}
})
// avoid: accessing ownProps without reason
const mapDispatchToProps = (dispatch, ownProps) => ({ // <-- wasteful
onClick: () => {
dispatch(honk()) // <-- not using ownProps
}
})
// avoid: passing ownProps from component
const mapDispatchToProps = (dispatch) => ({ // <-- ignore ownProps
onClick: (id) => { // <-- passes id from component
dispatch(honk(id))
}
})
// avoid: manually dispatching a thunk
const mapDispatchToProps = (dispatch, ownProps) => ({
onClick: (event) => {
event.preventDefault()
const { id } = ownProps
dispatch((_, getState) => { // <-- awkward; access getState
const state = getState()
const isAlive = selectAlive(state)
if (isAlive) {
dispatch(honk(id))
}
})
}
})
// prefer: auto-dispatching a thunk
const mapDispatchToProps = (dispatch, ownProps) => bindActionCreators({
onClick: (event) => (_, getState) => {
event.preventDefault()
const { id } = ownProps
const state = getState()
const isAlive = selectAlive(state)
if (isAlive) {
dispatch(honk(id))
}
}
}, dispatch)When you need access to the state, you will benefit from the way that bindActionCreators auto-dispatches your action creators. You can gain access to getState by returning a thunk.
- Access to
getState - Conditionally dispatch based on current state
- Combines the benefits of the short-hand and long-hand versions
- Potentially create unnecessary thunks
- Accidentally dispatching twice
- Accidentally dispatching undefined
import { honk, kill } from '../modules/goose/actions'
import { selectAlive } from '../modules/goose/selectors'
// prefer: access getState
const mapDispatchToProps = dispatch => bindActionCreators({ // <-- ignore ownProps
onClick: (event) => (_, getState) => { // <-- ignore inner dispatch; access getState
const state = getState()
const isAlive = selectAlive(state)
if (isAlive) { // <-- conditional dispatch
dispatch(honk())
}
}
}, dispatch)
// prefer: ignore getState
const mapDispatchToProps = (dispatch, ownProps) => bindActionCreators({ // <-- access ownProps
onClick: () => () => { // <-- ignore getState
const { id } = ownProps
dispatch(honk(id)) // <-- no explicit or implicit return
}
}, dispatch)
// prefer: mix short-hand and long-hand versions
const mapDispatchToProps = dispatch => bindActionCreators({
onClick: () => () => { // <-- long-hand; control payload
dispatch(honk())
},
onClose: kill // <-- short-hand; uncontrolled payload
}, dispatch)
// avoid: double-dispatch
const mapDispatchToProps = dispatch => bindActionCreators({
onClick: () => dispatch(honk()) // <-- whoops! dispatches the action twice
}, dispatch)
// avoid: awkward explicit return dispatch
const mapDispatchToProps = dispatch => bindActionCreators({
onClick: () => {
return honk() // <-- awkward, returned value is dispatched
}
}, dispatch)
// avoid: accessing ownProps without reason
const mapDispatchToProps = (dispatch, ownProps) => bindActionCreators({ // <-- wasteful
onClick: () => () => {
dispatch(honk())
}
}, dispatch)
// avoid: reassigning dispatch
const mapDispatchToProps = dispatch => bindActionCreators({
onClick: () => (dispatch, getState) => { // <-- avoid: reassigns dispatch
const state = getState()
const isAlive = selectAlive(state)
if (isAlive) {
dispatch(honk())
}
}
}, dispatch)Using bindActionCreators auto-dispatches your actions, which enables short-hand mapping of actions to props. If you don't care what your payload is or you prefer to set your payloads in your components, bound actions can be very convenient. Because of this, developers are probably more familiar with the short-hand notation.
Below we can see an example of both the short-hand and the long-hand versions. The dispatchProps object is expected to manage the dispatching of actions itself. The short-hand notation uses bindActionCreators to automatically bind all of your action creators while the long-hand version leaves that step up to the developer.
A careless developer may not notice the mistake below. In this example, nothing would ever be dispatched.
import { honk } from '../modules/goose/actions'
// works as expected
const mapDispatchToProps = {
onClick: honk // <-- yay: auto dispatches
}
// doesn't auto-dispatch
const mapDispatchToProps = dispatch => ({
onClick: honk // <-- whoops! doesn't dispatch
})At the top of this document we mention that a container wraps a component. Here we briefly explore what a react-redux application looks like from the perspective of a component.
Here we're showing the preferred example where our container reads useful values from ownProps and keeps the component in the dark about the id. For completeness, we're using prop-types to ensure that our ID
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import { honk } from '../modules/goose/actions'
import SomeButton from './SomeButton'
// prefer: bind ownProps
const mapDispatchToProps = (dispatch, ownProps) => ({
onClick: () => {
const { id } = ownProps
dispatch(honk(id)) // <-- bind the payload
}
})
const SomeButtonContainer = connect(
undefined,
mapDispatchToProps
)(SomeButton)
SomeButtonContainer.propTypes = {
id: PropTypes.string.isRequired // <-- prefer: type-check your container props
}
export default SomeButtonContainerimport React from 'react'
import PropTypes from 'prop-types'
const SomeButton = ({ children, onClick }) => {
return (
<button onClick={onClick}>
{children}
</button>
)
}
SomeButton.propTypes = {
children: PropTypes.node,
onClick: PropTypes.func
}
export default SomeButtonHere, we're moving the burden for binding props to the component. Now the component needs to pull in props it might not otherwise care about. Additionally, the component needs to rebind the id to the onClick function on every render. This isn't terribly expensive but it's work a component shouldn't be doing.
import { connect } from 'react-redux'
import { honk } from '../modules/goose/actions'
import SomeButton from './SomeButton'
// avoid: payload managed by component
const mapDispatchToProps = {
onClick: honk // <-- no control of payload
}
const SomeButtonContainer = connect(
undefined,
mapDispatchToProps
)(SomeButton)
export default SomeButtonContainerimport React from 'react'
import PropTypes from 'prop-types'
const SomeButton = ({ children, id, onClick }) => {
const boundOnClick = () => onClick(id) // <-- avoid: re-binds id when props change
return (
<button onClick={boundOnClick}>
{children}
</button>
)
}
SomeButton.propTypes = {
children: PropTypes.node,
id: PropTypes.string, // <-- extra prop
onClick: PropTypes.func
}
export default SomeButton
@wgao19 sure. Just saw this comment.