Skip to content

Instantly share code, notes, and snippets.

@dineshpanda
Forked from heygrady/mapDispatchToProps.md
Created July 19, 2021 07:55
Show Gist options
  • Save dineshpanda/8064444c628ae0ee6c115aed6a94312e to your computer and use it in GitHub Desktop.
Save dineshpanda/8064444c628ae0ee6c115aed6a94312e to your computer and use it in GitHub Desktop.
Redux containers: mapDispatchToProps

Redux containers: mapDispatchToProps

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.

Generic kitchen sink example

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.

With bindActionCreators

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.

Without bindActionCreators

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

Simplest example

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

What is connect?

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 a stateProps object.
  • mapDispatchToProps — dispatches actions; creates a dispatchProps object.
  • mergePropsnot commonly used. Merges the property objects from stateProps, dispatchProps and ownProps.
  • optionsnot commonly used. Options for deeper control over what how connect function operates.

Key point: this document is discussing the second argument, mapDispatchToProps.

What is a container?

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 connectmapStateToProps 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.

A quick note on mapStateToProps

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.

A quick note on mapDispatchToProps

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.

Example redux container:

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

Example react component:

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

What is an action creator?

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.

Example action creator

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.

What is mapDispatchToProps?

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.

Three versions of mapDispatchToProps

  • 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 functionnot commonly used. A factory function that returns a mapDispatchToProps function. Allows for manually memoizing the dispatchProps object.

Key point: The purpose of mapDispatchToProps is to create a dispatchProps object.

What is 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.

Standalone example of how mapDispatchToProps works

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

Using mapDispatchToProps

As noted above, the connect function specifies three ways to define your mapDispatchToProps argument.

These three versions are functionally equivalent

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).

Two functional long-hand versions

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 when connect initializes
  • Bind ownProps version — called whenever ownProps 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.

Why avoid the factory version?

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 memoized
    • mapDispatchToProps(dispatch) will only be called once, when connect is initialized
    • mapDispatchToProps(dispatch, ownProps) will be called whenever ownProps 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 time ownProps changes

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.

Why does the factory version exist?

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 your dispatchProps 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.

Example binding ownProps in a factory

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

Why avoid the short-hand version; why prefer the long-hand version

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.

These two versions are functionally equivalent

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

Object short-hand version

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.

Pros

  • Easy to write
  • Easy to maximize performance
  • Easy to dispatch thunks to access dispatch and getState

Cons

  • 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

Object short-hand examples

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

Functional long-hand version

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.

Pros

  • Access to dispatch
  • Access to ownProps
  • Easy to extend
  • Pressure to rename actions
  • Pressure to control payloads
  • Manually dispatch

Cons

  • Hidden tricks can impact performance

Functional long-hand examples

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)

Using bindActionCreators with the functional long-hand version

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.

Pros

  • Access to getState
  • Conditionally dispatch based on current state
  • Combines the benefits of the short-hand and long-hand versions

Cons

  • Potentially create unnecessary thunks
  • Accidentally dispatching twice
  • Accidentally dispatching undefined

Functional long-hand (with bindActionCreators) examples

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)

A quick note on auto-dispatching

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

Best practices: where to bind props?

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.

Prefer: binding props inside the container

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

Good container:

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

Good component:

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

Avoid: binding props inside the component

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.

Bad container:

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

Bad component:

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
const filterProps = (props, onlyPropNames = []) => onlyPropNames.reduce((newProps, propName) => {
newProps[propName] = props[propName]
return newProps
}, {})
export const shouldFactoryBindProps = (prevProps, nextProps, comparePropNames = []) => {
if (prevProps === undefined || (prevProps !== undefined && nextProps === undefined)) { return true }
return comparePropNames.some(propName => prevProps[propName] !== nextProps[propName])
}
export const createDispatchPropsFactory = (createDispatchProps, comparePropNames = []) => {
let prevOwnProps
let dispatchProps
return () => {
return (dispatch, ownProps) => {
const shouldBind = shouldFactoryBindProps(prevOwnProps, ownProps, comparePropNames)
if (shouldBind) {
dispatchProps = createDispatchProps(dispatch, ownProps)
prevOwnProps = filterProps(ownProps, comparePropNames)
}
return dispatchProps
}
}
}
export const createActionDispatchers = (createDispatchProps, comparePropNames = []) => {
const argsLength = createDispatchProps.length
const bindOwnProps = argsLength === 2
const shouldCreateFactory = !!comparePropNames.length
if (bindOwnProps && shouldCreateFactory) {
return createDispatchPropsFactory(createDispatchProps, comparePropNames)
}
return createDispatchProps
}
export default createActionDispatchers
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment