Using Redux with React Hooks - Should You Though?

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, but should you? 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”:

Deeply nested DOM tree due to components wrapped with connect function

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.

Why I wouldn’t recommend using Redux with Hooks

Hooks are great. They allow for using function components in ways that weren’t previously possible. When it comes to Redux, though, using the connect higher-order component is usually better for one big reason: separation of concerns.

When using the Redux Hooks, we only have one component, and that component is tightly coupled with Redux. The only way to test the component is to either mock out the Redux Hooks calls or wrap it in a <Provider> with a fake store instance.

connect, on the other hand, helps keep a separation between connected and non-connected components. One pattern that I’ve typically followed with Redux and React is to create my non-connected component and then wrap it with the connect higher-order component but still export both components from the file. I can then use the non-connected component in my tests and when prototyping with Storybook.

It’s for this reason that I’d generally recommend continuing to use connect. It’s not new and cool, but it isn’t going anywhere.