My inspiration for this blog post came from this video where Dan Abramov walks through the source code to react-redux
As frontend web developers, it’s not uncommon that we follow well-specified patterns - often blindly. The frontend landscape is changing rapidly, and sometimes there isn’t time to investigate why we use a specific pattern; we just know we should.
One widely used pattern in react-redux applications looks like this
I’ll assume you know how to implement this pattern, but why do we use it and how does it work under the hood?
React and Redux are two completely independent tools that have nothing to do with each other. React is a tool for creating user interfaces in the browser. Redux is a tool for managing state. Either tool can be used without the other. We often use them together because they both solve separate but very important and closely related problems. The purpose of react-redux is to get these two tools to talk.
But first, what would we do without react-redux? How would React and Redux talk?
More precisely, how do we ensure that a React component re-renders when the Redux store changes? The answer lies in Redux’s subscribe API.
If we insert the above boilerplate into every one of our React component’s, then every component could have access to the store and would be informed through a subscription the moment the store’s state changes. This configuration has three flaws.
- The boilerplate of subscribing and unsubscribing to the store is highly error prone and unnecessarily verbose.
- All of our React component’s are dependent upon knowledge of the Redux store. This is a complete failure of separation of concerns.
- Every component is dependent upon the entirety of the store’s state tree. This means that whenever an action is dispatched,
setStateis called on every mounted component, causing each one to re-render, regardless of whether its render function depends on the store state that changed. Woah! Let that sink in for a moment.
Let’s write a rudimentary implementation of connect that resolves the first problem.
Typically, we invoke
connect like this:
connect takes in two functions as arguments and returns a function. Yes, you heard me,
connect returns a function, not a component. Suppose I invoke
connect and neglect to pass in a component.
connect will return to me a function. It’s that function that takes in my component (
connect is implemented this way as opposed to simply taking in 3 arguments to support decorator syntax. The Dan Abramov video I linked above explains this.)
Thus, the very first few lines of
connect must look like this:
And what does the function we returned above do? This function is implemented as a higher order component (HOC). A HOC is a function that takes in a component as a parameter and returns a new component. The new component is generally a modified or augmented version of the original component.
If we were to run the above
connect function on a component, the connected component would behave identically to original component. Furthermore, we could nest
connect as many times as we want
and still never distort the behavior of the original component. Our current implementation is effectively idempotent.
Our next step is to eliminate some of the boilerplate code. We don’t want to have to subscribe to the store every time we create a new component, so let’s have our new
connect function do it instead.
We just made huge progress! Now, whenever we invoke
we get a component that is subscribed to state changes on the store, and this state will be passed down to our component as props.
Our connected components still all depend on the entirety of the store’s state tree. Look up above, the entire state is passed down as props to every connected component. To reiterate, this means that if any piece of the store’s state is updated, our component will re-render.
This is where
mapStateToProps comes to the rescue.
mapStateToProps takes as its argument the store’s state, and it allows us to return the particular pieces of the store’s state that a component depends on. It then passes that state as props to our component instead.
All we did was insert a call to
mapStateToProps, allowing us to make each connected component dependent upon only the state it cares about, as defined by the return value of
mapStateToProps is a wonderful form of explicit documentation, clearly stating the slices of the state tree each component depends on. Unfortunately, our change does not fix the efficiency problems noted above. More on that below.
An astute reader might note that
mapStateToProps actually takes two arguments: the first is a copy of the store’s state, and the second are the props that are originally passed down to
react-redux does not pass these down to the wrapped component by default as we do in the example immediately above. Let’s modify our implementation to mirror
Now the implementer of
mapStateToProps can choose which of
WrapperComponent‘s props it would like to keep and which it would like to disregard.
mapDispatchToProps is designed to eliminate React’s dependency upon Redux. If we were to use the above implementation of
connect, every component that dispatch’s an action must import
store.dispatch, and the implementation would look like this:
The above component ‘knows’ that it is part of a Redux application because it is explicitly referencing the store to dispatch actions. But we should always try to minimize the interaction of different pieces of architecture, esspecially when they have no need to interact. Ultimately, React components should not been intertwined with Redux code!
connect resolves this problem for us by injecting the
store.dispatch dependency into
mapDispatchToProps, allowing us to explicitly define functions that dispatch actions without requiring that our presentation components have a dependency on the store. Just as the return value of
mapStateToProps is passed down to
WrappedComponent, the return value of
mapDispatchToProps will be passed down as well.
We never actually fixed any of the performance issues noted above. The crux of the problem is that every time the store updates,
WrapperComponent re-renders (because of its Redux store subscription that calls
setState) and that means
WrappedComponent re-renders. This re-rendering happens despite the fact that
WrappedComponent‘s props might be unchanged between two invocations of
setState. In fact, this scenario is highly probable and will occur whenever a piece of state in the store changes that your component does not depend on (aka, a piece of store state not returned from from
React has a handy lifecycle method called
shouldComponentUpdate that allows us to return a boolean that indicates whether a component should re-render. In essence, if we implement this method on
WrapperComponent and it returns
false, then React will not re-render
WrapperComponent. And it follows that
WrappedComponent won’t re-render either.
So, in the above scenario, when
setState, React first calls the
shouldComponentUpdate method to see if a re-render should actually happen. Let’s implement it below.
I’ve created a demo here. Open the console and prove to yourself that
shouldComponentUpdate is doing its job.
I should note that this is not exactly what react-redux does because of edge cases, but the concept is still the same.
Now our wrapper and wrapped components will only re-render when the props returned from
mapStateToProps change! This is a huge performance gain. This implementation of
connect explains why adherence to immutability is so important in redux’s reducers. If you fail to respect immutability, the shallow comparison in the
WrapperComponent will likely return
false, causing your connected component to not re-render when it should.
connect method is remarkably simple and only performs a handful of operations.
- It manages our component’s subscription to the store so that our component can update when the store updates.
- It allows us to explicitly define the slice of state our component is dependent upon using
- It gives our component access to
store.dispatchwithout requiring a direct dependency on the store.
- It defines
shouldComponentUpdate, ensuring that our components only re-render when the store state they depend on changes.
I hope you found this article helpful. Please feel free to email me and reach out if you have questions. I put a gist online containing the same code as the demo.