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 astateProps
object.mapDispatchToProps
— dispatches actions; creates adispatchProps
object.mergeProps
— not commonly used. Merges the property objects fromstateProps
,dispatchProps
andownProps
.options
— not commonly used. Options for deeper control over what howconnect
function 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 SomeButtonContainer
Here'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 SomeButton
You 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
mapDispatchToProps
function. Allows for manually memoizing thedispatchProps
object.
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
dispatchProps
object 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
ownProps
version — only called whenconnect
initializes - Bind
ownProps
version — called wheneverownProps
changes
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,
dispatchProps
is already memoized - If you use the long-hand version,
dispatchProps
is also already memoizedmapDispatchToProps(dispatch)
will only be called once, whenconnect
is initializedmapDispatchToProps(dispatch, ownProps)
will be called wheneverownProps
changes
- 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
dispatchProps
creator will be called every timeownProps
changes
- 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
dispatchProps
object - must have noisy values in
ownProps
that are irrelevant to yourdispatchProps
object
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
dispatch
andgetState
- 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 SomeButtonContainer
import 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 SomeButton
Here, 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 SomeButtonContainer
import 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