Hiding Code - Less Redux Is More
One interesting thing about the Redux pattern is how much of it can just… go away. Here we look at how.
The first time I encountered Redux, I have to admit I wasn’t impressed. For a start, I was trying to learn React at the same time, and tangling the two technologies together in my head made it a bit challenging.
Mostly, though, it was the sheer amount of boilerplate that got to me. It was just… so much cruft. By comparison to something like Ember or Angular, which already have a store available out of the box, the amount of pageantry required just to get a basic state management seemed absurd.
It took me some time to get the hang of Redux. Certainly it took me some time to get comfortable enough to experiment and try to improve on what I’d learned. And when I was able to do so I found that there were techniques and approaches that really made Redux a lot more manageable.
The examples given in articles or even the official documentation are often either a bit out of date or are serving as a lowest common denominator for syntax.
But given a proper understanding of Redux’s patterns, some modern JavaScript techniques and a few little hidden Redux features, we can optimise our store boilerplate nearly out of existence.
Let’s start with a basic interface. I’m not going to show how this interface looks, it doesn’t matter. I’m also not going to optimise a damn thing. In reality this component would be broken into a number of different components, each handling their own bits of state. I’m not going to even show the component, but essentially it’s a list of comments and all a form to make a new comment.
So to make it clear, we don’t really care about optimising this component, that’s why it’s not here. Its architecture and design aren’t the issue. It’s been eliminated entirely to illustrate how to better optimise the Redux part of it.
As such I’m using a pattern here where the component and the Redux elements are separate files. When I first implemented Redux it was like this, as it let me isolate the Redux elements to a file and leave the existing and working component untouched. I don’t do this anymore but it still serves to isolate the Redux-related code for demonstration.
import React, { Component, Fragment } from 'react';
import { connect } from 'react-redux';import { fetchComments, saveComment, showNewCommentForm, likeComment } from '../actions/comments';import Comments from './Comments';function mapStateToProps(state) {
return {
comments: state.comments,
user: state.user
}
}function mapDispatchToProps(dispatch) {
return {
fetchComments: () => {
dispatch(fetchComments());
},
saveComment: () => {
dispatch(saveComment());
},
showNewCommentForm: () => {
dispatch(showNewCommentForm());
},
likeComment: () => {
dispatch(likeComment());
}
}
}export default connect(mapStateToProps, mapDispatchToProps)(Comments);
This is very long. There is a lot of code here. But we’re not done. There are some actions as well.
export function fetchComments() {
return dispatch => {
return commentService.getComments()
.then(
response => dispatch(receiveComments(response.comments))
)
}
}export function receiveComments(comments) {
return {
type: 'RECEIVE_COMMENTS',
payload: comments
};
}
If it seems like I’m making a strawman here, by deliberately posting in a badly written action creator… I literally copied it from a random application in production now.
There are a few others, but I won’t go into them, because all the same really applies to them. Also worth noting I’m just using a string for the action type. I’ve tried having action type constants but to be honest it was more trouble than it was worth. Just a matter of opinion.
I won’t bother with examples of the reducers. I haven’t found all that much you can do with them in terms of optimisation.
Let’s Get Hacking State
To start with, we’ll go with the least complex part. Let’s pull out the mapStateToProps
function and look at that by itself.
function mapStateToProps(state) {
return {
comments: state.comments,
user: state.user
}
}
Not much to say here. It’s a pretty standard function doing a pretty standard thing. But… that’s just the point, right? It is just a standard function.
So that means any way we’d improve a standard function would improve this one. We’ll start off with a fat arrow function just because it’s a bit shorter.
const mapStateToProps = state => {
return {
comments: state.comments,
user: state.user
}
}
Then why not destructure our state? I mean, we don’t actually care about the whole thing, just some bits.
const mapStateToProps = ({user, comments}) => {
return {
comments: comments,
user: user
}
}
And of course, if we can destructure out we can do the same with our return object and then make it one line.
const mapStateToProps = ({user, comments}) => {
return { comments, user }
}
There’s one last bit of cruft here. We don’t really need our explicit return, nor the brackets around the function. We can’t just return an object literal though, the interpreter would see it as this invalid mess.
const mapStateToProps = ({user, comments}) => {
return comments, user;
}
But if we wrap brackets around the literal it’s golden.
const mapStateToProps = ({user, comments}) => ({comments, user});
There’s a kind of pleasant symeAs a last point here it doesn’t have to be called mapStateToProps and in fact doesn’t even need to be in this file. It is just a function, and could easily be imported from somewhere else.
import { commentState } from '../redux/standardStates';
Taking Action
Let’s move on to our actions. Our first one is a “thunk” — an action that returns a function instead of an action object.
export function fetchComments() {
return dispatch => {
return commentService.getComments()
.then(
response => dispatch(receiveComments(response.comments))
)
}
}
Again, it’s just a function, so let’s start with making it a fat arrow.
export const fetchComments = () => {
return dispatch => {
return commentService.getComments()
.then(
response => dispatch(receiveComments(response.comments))
)
}
}
Then we have a bit of cruft around our promise handling. It’s worth nothing that while an action can’t be an async function, the thunk inside it can be. This means we can use async/await and clean up this, destructuring it at the same time.
export const fetchComments = () => {
return async dispatch => {
const { comments } = await commentService.getComments();
dispatch(receiveComments(comments));
}
}
Our receiver of these comments is also just a function. We’ll start off with it converted to a fat arrow.
export const receiveComments = comments => {
return {
type: 'RECEIVE_COMMENTS',
payload: comments
};
}
We’ve kind of created a problem for ourselves, because we actually need our comments called payload for our reducer. So we need to do this to be named like we have here. But we don’t really. We can just call it payload as we pass it in, and make the destructuring easier.
export const receiveComments = payload => {
return {
type: 'RECEIVE_COMMENTS',
payload
};
}
And again, a single line is fine.
export const receiveComments = payload => ({ type: 'RECEIVE_COMMENTS', payload });
Though this doesn’t fit well on a single line here it’s fine in any editor.
Mapping Dispatch
Finally, let’s take another look at the most complex bit of code here, mapping the actions and dispatching into our component props.
function mapDispatchToProps(dispatch) {
return {
fetchComments: () => {
dispatch(fetchComments());
},
saveComment: () => {
dispatch(saveComment());
},
showNewCommentForm: () => {
dispatch(showNewCommentForm());
},
likeComment: () => {
dispatch(likeComment());
}
}
}
So the first step should be obvious. First of all, we can make it a fat arrow function, but additionally it doesn’t need the brackets on the dispatch functions.
const mapDispatchToProps = dispatch => {
return {
fetchComments: () => dispatch(fetchComments()),
saveComment: () => dispatch(saveComment()),
showNewCommentForm: () => dispatch(showNewCommentForm()),
likeComment: () => dispatch(likeComment())
}
}
It should also be obvious that we’re doing a lot of duplication here. There has to be a way to automate that. And there is. The function bindActionCreators
literally does exactly this.
const mapDispatchToProps = dispatch => {
return bindActionCreators(
{fetchComments, saveComment, showNewCommentForm, likeComment},
dispatch
);
}
Much shorter. Normally I’d put it on one line but for the sake of this article broke it up. But wait. We’re not done.
In fact, not only is the above behaviour simpler, it’s actually built into connect
if it’s passed an object of action creators. This is an important bit of info that isn’t obvious on first learning Redux.
So we can just as easily do this.
const myActions = {fetchComments, saveComment, showNewCommentForm, likeComment};export default connect(mapStateToProps, myActions)(Comments);
You don’t even need to make the myActions
const, you could just put it straight into the connect
argument, but I needed the space.
There’s one last improvement that we can make, though. Our actions are imported above, then split out, and then put back into an object. Couldn’t we just… not?
import * as CommentActions from '../actions/comments';...export default connect(mapStateToProps, CommentActions)(Comments);
There may be reasons you can’t do this. Maybe you don’t want all the actions in the actions module, but maybe you can deal with a few unused props for the vastly simpler code. Additionally, maybe you need some other random action from elsewhere. We can deal with that pretty easily.
import * as CommentActions from '../actions/comments';
import {logIn} from '../actions/default';...export default connect(mapStateToProps, {...CommentActions, logIn})(Comments);
The End Result
In the end we get some significant code improvements. We start with a fairly long block of container code.
import React from 'react';
import { connect } from 'react-redux';import { fetchComments, saveComment, showNewCommentForm, likeComment } from '../actions/comments';import Comments from './Comments';function mapStateToProps(state) {
return {
comments: state.comments,
user: state.user
}
}function mapDispatchToProps(dispatch) {
return {
fetchComments: () => {
dispatch(fetchComments());
},
saveComment: () => {
dispatch(saveComment());
},
showNewCommentForm: () => {
dispatch(showNewCommentForm());
},
likeComment: () => {
dispatch(likeComment());
}
}
}export default connect(mapStateToProps, mapDispatchToProps)(Comments);
And it can now be written as this.
import React from 'react';
import { connect } from 'react-redux';import * as CommentActions from '../actions/comments';
import Comments from './Comments';const mapStateToProps = ({user, comments}) => ({comments, user});export default connect(mapStateToProps, CommentActions)(Comments);
And our actions go from this
export function fetchComments() {
return dispatch => {
return commentService.getComments()
.then(
response => dispatch(receiveComments(response.comments))
)
}
}export function receiveComments(comments) {
return {
type: 'RECEIVE_COMMENTS',
payload: comments
};
}
to this
export const fetchComments = () => {
return async dispatch => {
const {comments} = await commentService.getComments();
dispatch(receiveComments(comments));
}
}export const receiveComments = payload => ({type: 'RECEIVE_COMMENTS', payload});
The actions only go from 14 lines to 8, which is significant but not enormous. Where it does make a difference is when there are a number of them, and the tiny receivers action creators can stack together nicely.
But our modifications to the container code take Redux’s boilerplate from 31 lines to only 9, including spaces. In a real-world scenario it’s reasonable to assume that the addition of Redux boilerplate to an existing component could add as little as five or six lines of code. (The discrepancy is in the fact that we wouldn’t need to add a line to import React, import or export the component, etc.)
This is a significant improvement over the reams of code I often see polluting React containers. Shorter code blocks are easier to understand, faster to read, and easier to spot errors in. It also becomes easier to see repeating code patterns that could be refactored out.
In my opinion the Redux pattern is inherently and irreducably complex, which can make it somewhat challenging. By that I mean failing any part of it will leave you with nothing at all. What it doesn’t have to be in addition is verbose. That’s something that we can improve.