React Redux
💖
Introduction
React and Redux are two immensely popular JavaScript libraries which are commonly paired together in order to develop complex applications that are scalable and easy to reason about. In this article I'll capture my understanding of how Redux works side-by-side with React (via the react-redux
library) in order to tame your application's complex data flow.
Redux
In a previous article, we built a simple app using Redux and we intentionally did so without a UI framework in order to explore Redux's APIs in isolation. We learned that actions are plain old JavaScript objects, a reducer is nothing more than a function, and that the API of the Redux library itself boils down to:
createStore
- to create the storestore.dispatch
- to dispatch an actionstore.subscribe
- to listen for updates to the state
Now, we'll build on that foundation by examining how React Redux provides helpers on top of those APIs that make it super easy to use Redux with React.
React Redux
React Redux is a library that helps us to connect our React components to a Redux store.
React Redux doesn't replace Redux, it simply provides a few helpful tools to make it easier to use Redux's store.dispatch
and store.subscribe
within a React application.
Actions and Reducers
Actions and reducers are still just plain objects and functions. We'll build those like we did in the previous article without any additional logic from React Redux.
Provider
The first helper provided by React Redux is the Provider
component. Provider
uses React Context to make your Redux store
available to its children. We typically add it at the root of our application so that the store is accessible to every one of our components!
We create the store
just like before: with Redux's createStore
method.
Connect
Next, we have the connect
Higher Order Component. Now that React Redux exposes hooks, this utility is going to be used less often, but we'll cover it here anyway because it's still heavily used in production applications.
Connect, like all other Higher Order Components, is a function (or a function which produces a function in this case) which takes in your component as an argument and returns a new, augmented component in return.
The augmentation that Connect performs is to wrap your component in a Context Consumer (so that it can access the store
from Provider
) and gives your component access to the store's state and dispatch method by injecting props.
Here's the same counter app that we built in the Redux article but using React Redux's connect
utility.
You tell Connect which pieces of state that your component cares about via the mapStateToProps
function which accepts the current state, and returns a trimmed-down version. Connect then injects that trimmed-down state into your component as props.
Connect also sets up a store-subscription (via store.subscribe
) so that it can listen for changes to the props that your component cares about and can re-render the component for you whenever those props change.
You can also get access to store.dispatch
in your component by providing the mapDispatchToProps
function which accepts the store.dispatch
function itself, and returns a set of functions which use dispatch to communicate with the store.
useDispatch and useSelector
Next, we have useDispatch
. It's a hook that reaches in to the Context (from Provider
) to get the store
and it returns the store.dispatch
method so that you can dispatch an action from within your React component.
And then we have useSelector
which, again, uses the Context from Provider
to get the store
, but rather than returning the dispatch
method, it returns the current state from the store. useSelector
accepts a mapState
function which trims down the state to exactly what the component needs (just like connect
s mapStateToProps
).
Finally, useSelector
sets up a store-subscription (like connect
's) so that your component will re-render whenever certain state-changes occur.
Here's the counter app one more time, using useDispatch
and useSelector
rather than connect
:
Referential Equality and Re-Renders
In the Redux article we talked about how it's super important to return a new object from your reducer so that Redux can perform a quick referential-equality check to determine if the state changed.
React Redux adds another place where we'll need to be mindful of referential equality.
Every time Redux broadcasts that a piece of state changed, all of your connect
s and useSelector
s will quickly run their mapState
functions and they'll try to determine if their component should re-render by comparing the current result of mapState
(using the current state) to the new result of mapState
(using the new state). Since mapState
always returns a new object, the equality check must go slightly deeper than the check that Redux does at the top level of your state. Which is to say, rather than simply doing:
it is necessary for connect
and useSelector
to go a bit deeper:
Even though we're going one level deeper and comparing each of the keys of the mapped state, we're still not recursively analyzing every single child, and we're still doing a referential equality check on each key.
In the Redux article, we made sure that we always returned a new object from our reducer in order to indicate that some state had changed, but with React Redux we need to do a bit more:
if you change any nested state, you need to update the references to *every single parent* all the way up to the root state object
This way, your connect
s and useSelector
s can mapState to any arbitrary chunk/level/sub-tree of the state and will always update appropriately when the state changes.
Here are some examples. Let's say that we want to update a deeply-nested value in our state tree: state.foo.bar.baz
In this example, we changed the object itself, the parent, grandparent, and root. We started with the object that we needed to change and we walked all the way up to the root and changed the ref to each parent on the way.
An alternative implementation might look like this:
In this example, we used the spread operator to "clone" the foo object, so the grandparent (foo
) got a new reference. We returned a new object from the reducer, so the root object changed, and we overwrote the value of baz to our new value, so the object itself got a new reference, but what about bar
!? The ref to bar
actually didn't change in this example.
Even thought the root-state-object changed, and Redux would broadcast the state change, connect
and useSelector
would ignore the change if they were subscribed directly to bar
.
e.g. if you had a function that did this:
it would not re-render when foo.bar.baz
changed because the ref to bar
didn't get updated!
If the pattern shown in the "good" example of updating refs all the way up to the root seems tedious or confusing, you may be interested in a library like immer which abstracts these concerns away from the developer and lets you write nice, terse reducers that feel leverage the readability of impertive APIs without violating Redux's rules around immutability and referential equality.
Wrap Up
We've seen how React Redux provides helpers on top of Redux's store.dispatch
and store.subscribe
to make them easier to use inside of a React application. Provider
exposes the store to your components; connect
and useDispatch
+useSelector
make it easy for individual components to subscribe to a single piece of state (only re-rendering when necessary) and for individual components to dispatch actions directly to the store.
We've explored Redux's and React Redux's usage referential equality to perform super fast comparisons to see if state has changed, and we understand the patterns that have emerged within reducers that help us avoid the related "gotchas"
Thanks for reading!
Edit on GitHub