Exporting mapStatesToProps in React Redux

July 2017

I feel a lot is being written about JavaScript, and I find it difficult to contribute with something genuinely original and useful. This is quite possibly also not original as it sort of came naturally from working intensely with React Redux over the past few years, so I can only imagine that others have come to similar conclusions (and possibly built a library or two around it); but at least it is useful and I have not seen this mentioned anywhere.

The problem

If you’ve worked with React Redux for a while (especially if you’ve refactored large amounts of code) then you know to try your best to keep every component as pure as possible. Small amounts of internal state, such a whether a navigation is open or not can be fine; but anything more than that will be more difficult to present, test, refactor, and reason about.

In React Redux you can pull out data from the global state from any connected component, so it is common to see people wrap pure (or “dumb”) components in a connected container component. Let’s see a quick example.

First the pure component. It takes a name, and a function and greets the person. The person can fire off a “sayHello” action back by clicking a button.

// greeting.js
const Greeting = props => (
    <div>
        <p>Hello {props.name}</p>
        <button onClick={e => props.sayHello()}>Say hello</button>
    </div>
)

Greeting.propTypes = {
    name: PropTypes.string.isRequired,
    sayHello: PropTypes.func.isRequired
}

export default Greeting

The container component does not have any real UI; its purpose is to fetch whatever is needed from the state based on some parameter(s); in this case a userId.

// greeting-container.js
import Greeting from './greeting'
import {sayHello} from './actions'

const GreetingContainer = props => (
    <Greeting name={props.name} sayHello={props.sayHello} />
)

GreetingContainer.propTypes = {
    name: PropTypes.string.isRequired,
    sayHello: PropTypes.func.isRequired
}

const mapStateToProps = (state, props) => ({
    name: selectors.getNameById(state, props.userId)
})

export default(mapStateToProps, {sayHello})(GreetingContainer)

In case we want to create another pure component that makes use of this one, then we’d use the greeting.js version and make sure to provide everything that it needs and pass that need upwards to whoever is using our new component.

The problem is that this quickly gets very unwieldy. It gets unwieldy in two ways:

  1. propTypes grow. One component I had had a propTypes object spanning 32 lines. While using this component was still simple enough, just give it what it needs; it felt a bit like you were fetching half the app state and a quarter of the actions for it to run. It would’ve of course been even worse if said component had been used in many places.
  2. If something on the bottom changes its requirements then you’ll be hammering at your keyboard for a while. As an example we had our Avatar component, a very central piece of the UI, change its propTypes from a required photo url, to a non-required photo url and a required first name and non-required last name (to produce some initials to show in case the user did not have a photo). It took a while to refactor.

Again, both situations above are simple to work with (and that is Redux’ stregth); everyone gets it. It just felt like a lot of boilerplate for not that much benefit.

Solution

The goal is to get to a point where we have nested pure components, but where we also aren’t dealing directly with each and every prop of every sub component on every level.

We want to err on the side of caution here though, because there is a very fine line that we should not cross. Like I wrote just above, dealing with everything at every level is very explicit, and explicitness is good. Where we don’t want to go is magic-land where everything is implicit and new people on the team have no way to find their way around.

With that in mind, let’s do a bit of refactoring on the Greeting component above. First we remove the notion of a component being either pure or connected by exporting both versions (and its mapStateToProps) from the same file.

// greeting.js
import {sayHello} from './actions'

export const Greeting = props => (
    <div>
        <p>Hello {props.name}</p>
        <button onClick={e => props.sayHello()}>Say hello</button>
    </div>
)

Greeting.propTypes = {
    name: PropTypes.string.isRequired,
    sayHello: PropTypes.func.isRequired
}

export const mapStateToProps = (state, props) => ({
    name: selectors.getNameById(state, props.userId)
})

export default connect(mapStateToProps, {sayHello})(Greeting)

Now, depending on what we import, we have two ways of using the same component. If we do not care about purity, then we can just import it like we did above and move on with our day.

It gets more interesting when we are working with the pure version, because now we have the mapStateToProps to help us.

// profile.js
import {Greeting, mapStateToProps as mapGreetingProps} from './greeting'
import {sayHello} from './actions'
// ...
const Profile = props => (
    // ...
    <Greeting sayHello={props.sayHello} {...props.greetingProps} />
)
// ...
const mapStateToProps = (state, props) => ({
    greetingProps: mapGreetingProps(state, {userId: 'foo'})
})
export default connect(mapStateToProps, {sayHello})(Profile)

So now we are not dealing directly with a name. Sure, if we want, we can pass it in, essentially overriding whatever came out of mapGreetingProps, but it will default to whatever the component asked for itself.

This is exactly what we wanted. We got rid of dealing with every piece of the props passed down while remaining explicit about what we are doing. Perhaps even more so: The different props are grouped under their respective keys (greetingProps above).

If you were to console.log the props within Profile you’d see something like

{
    greetingProps: {
        name: 'John',
        sayHello: function () { ... }
    },
    // + whatever `profile` needs
}

What about the action?

We may no longer have to deal directly with the data that we are passing down to it, but we are still dealing with the actions. Can we change that somehow?

We can, but we need to go back to our original greeting.js first and dispatch the action in a slightly different way.

// instead of this
<button onClick={e => props.sayHello()}>Say hello</button>

// we do
import {sayHello} from './actions'
<button onClick={e => props.dispatch(sayHello())}>Say hello</button>

Slightly more verbose, but more explicit, and now we only rely on dispatch, no matter how many different actions we want to dispatch. This means that greeting.js itself has to import actions.sayHello, but as long as that action itself is pure1, then that’s okay. We are not breaking its purity.

Using it, we can change profile.js accordingly

// profile.js
// ...
const Profile = props => (
    // ...
    <Greeting dispatch={props.dispatch} {...props.greetingProps} />
)
// ...
// if we provide no second argument, `dispatch` is injected instead.
export default connect(mapStateToProps)(Profile)

What about propTypes?

PropTypes.shape is eminent here. I will change the Profile to include a few more components and this time provide the propTypes so you can get an idea of what it will all look like.

// profile.js
import {Greeting, mapStateToProps as mapGreetingProps} from './greeting'
import {TodoList, mapStateToProps as mapTodoListProps} from './todo-list'
// ...

export const Profile = props => (
    // ...
    <img src={props.photo} />
    <Greeting dispatch={props.dispatch} {...props.greetingProps} />
    <TodoList dispatch={props.dispatch} {...props.todoListProps} />
)

Profile.propTypes = {
    dispatch: PropTypes.func.isRequired,
    photo: PropTypes.string.isRequired,
    greetingProps: PropTypes.shape(Greeting.propTypes).isRequired,
    todoListProps: PropTypes.shape(TodoList.propTypes).isRequired
}

export const mapStateToProps = (state, {userId}) => ({
    greetingProps: mapGreetingProps(state, {userId}),
    todoListProps: mapTodoListProps(state, {userId}),
    photo: selectors.getPhotoFromId(state, userId)
})

export default connect(mapStateToProps)(Profile)

Closing

This isn’t as battle tested as I would have liked yet; but I will start doing my react components like this from now on, and hopefully will gain some experience with it. Error messages seem good when props are missing or wrong, and my initial impression is that the composability is amazing. My only concern is that the components may not be memoized very well since a lot of that, as I understand it, is handled by connect. Only time will tell how well this performs in the long run.


Notes.

  1. If you are using thunks in your project, then I suggest you have a look at “withExtraArgument” in the docs to keep your actions pure and testable despite calling your api and whatnot.