React Redux recently released version 7.1, which includes long
awaited support for React Hooks. This means that you can now ditch the connect
higher-order component and use Redux with Hooks in your function components.
This post will take a look at how to get started using Redux with Hooks and then
explore some gotchas of this approach.
What are Hooks?
Hooks were added to React in 16.8 and allow you to access things like state, React lifecycle methods, and other goodies in function components that were previously only available in class components.
For example, a React class component like this:
class Count extends React.Component {
state = {
count: 0
};
add = () => {
this.setState({ count: this.state.count + 1 });
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.add}>Add</button>
</div>
);
}
}
Could be rewritten as a function component using Hooks like this:
const Count = () => {
// state variable, initialized to 0
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Add</button>
</div>
);
};
The code is more concise and allows teams to make more use of function components without having to convert them to class components as soon as they need state or access to the React lifecycle. This post isn’t really about Hooks in general, so I’d check out the excellent Hooks documentation if you’d like to learn more.
How to use Redux with Hooks
React Redux now includes its own useSelector
and useDispatch
Hooks that can
be used instead of connect
.
useSelector
is analogous to connect
’s mapStateToProps
. You pass it a
function that takes the Redux store state and returns the pieces of state you’re
interested in.
useDispatch
replaces connect
’s mapDispatchToProps
but is lighter weight.
All it does is return your store’s dispatch
method so you can manually
dispatch actions. I like this change, as binding action creators can
be a little confusing to newcomers to React Redux.
Alright, so now let’s convert a React component that formerly used connect
into one that uses Hooks.
Using connect
:
import React from "react";
import { connect } from "react-redux";
import { addCount } from "./store/counter/actions";
export const Count = ({ count, addCount }) => {
return (
<main>
<div>Count: {count}</div>
<button onClick={addCount}>Add to count</button>
</main>
);
};
const mapStateToProps = state => ({
count: state.counter.count
});
const mapDispatchToProps = { addCount };
export default connect(mapStateToProps, mapDispatchToProps)(Count);
Now, with the new React Redux Hooks instead of connect
:
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import { addCount } from "./store/counter/actions";
export const Count = () => {
const count = useSelector(state => state.counter.count);
const dispatch = useDispatch();
return (
<main>
<div>Count: {count}</div>
<button onClick={() => dispatch(addCount())}>Add to count</button>
</main>
);
};
I like that using Redux with Hooks is a little bit simpler conceptually than
wrapping components in the connect
higher-order component. Another benefit of
not using the higher-order component is that you no longer get what I call
“Virtual DOM of death”:
I suggest reading the full Redux Hooks documentation for more information.
useSelector gotchas
useSelector
diverges from mapStateToProps
in one fairly big way: it uses
strict object reference equality (===
) to determine if components should
re-render instead of shallow object comparison. For example, in this snippet:
const { count, user } = useSelector(state => ({
count: state.counter.count,
user: state.user,
}));
useSelector
is returning a different object literal each time it’s called.
When the store is updated, React Redux will run this selector, and since a new
object was returned, always determine that the component needs to re-render,
which isn’t what we want.
The simple rule to avoid this is to either call useSelector
once for each
value of your state that you need:
const count = useSelector(state => state.counter.count);
const user = useSelector(state => state.user);
or, when returning an object containing several values
from the store, explicitly tell useSelector
to use a shallow equality
comparison by passing the comparison method as the second argument:
import { shallowEqual, useSelector } from 'react-redux';
const { count, user } = useSelector(state => ({
count: state.counter.count,
user: state.user,
}), shallowEqual);
There is a section in the Redux Hooks documentation that covers this in more detail.
Redux with Hooks vs. Connect higher-order component
Note: this section has been updated since the article was first published. I
originally recommended sticking with connect
over the Redux Hooks but have
since come to prefer using the Redux Hooks.
Hooks are great. They allow for using function components in ways that weren’t previously possible, and the community is clearly moving in the direction of using function components and hooks when possible. The React documentation states:
We intend for Hooks to cover all existing use cases for classes, but we will keep supporting class components for the foreseeable future.
While there is no need to race off and convert all your existing code, our
recommendation for new code is to generally prefer function components and Hooks
over using class-based components and higher-order components. Using the Redux
Hooks aligns with this recommendation and provides other key benefits
over connect
.
The main benefit of using the Redux Hooks is that they are conceptually simpler
than connect
. With connect
, you are wrapping your component and injecting
props into it. This can make it difficult to determine in the component which
props come from Redux and which are passed in. If you are using TypeScript,
correctly defining the types of connected components can be quite a chore for
this reason. The Redux Hooks, on the other hand, are just regular hooks that
don’t modify the public interface of your component. In TypeScript projects, I
create my own useSelector
that is typed to my store, and then I get
type-checking everywhere I use it. Easy!
While the Redux Hooks have many benefits, there is one benefit of using
connect
instead of the Redux Hooks, and that is that it keeps your component
decoupled from Redux. You can then test the component or prototype it in tools
like Storybook without having to connect the component. An earlier version of
this article recommended continuing to use the connect
middleware instead of
using the Redux Hooks for this reason. While this reasoning is still valid to an
extent, I’ve since found that in tests and prototyping tools, I almost always
wrap my components in a Redux Provider
and test them in the context of being
connected to Redux anyway. It’s just easier since my component might have a
descendent that is connected to Redux and therefore still need to be connected.
React Hooks are a useful new feature, and React Redux’s addition of Redux-specific hooks is a great step toward simplifying Redux development.