useSelector vs connect (react-redux)

 (Updated)6 min read

React-redux hooks like useSelector() and the connect() can have the same outcomes. The main difference between them is their ability to nudge (guide) the way you write your components. Understanding what each of them optimizes for is essential to helping you choose between them.


Hooks (useSelector) Connect
Connect

Unit testing and separation of concerns

A lens we can use to understand the differences between hooks and the connect() is unit tests: How they differ for each and in-turn what direction of code style they nudge you towards.

It becomes clear how organized your code is by its concerns when writing tests for it. When following the Separation of concerns principle your code becomes easier to test and reuse. There's a spectrum of Separation where maintainability becomes a problem at the extreme ends.

Separation of Concerns Spectrum

When "Not Separated" we have large files where it's hard to reuse any one piece.

Redux hooks such as useSelector() reduce separation because the components include the redux glue code. This can seem ironic because React hooks are a tool to help with separation and code reuse, but in react-redux they do couple redux to our components more than connect().

When "Very Separated" we have small pieces of code and it's a nightmare to figure out how they all work together.

Neither fall at the extreme ends.

connect() testing

When making use of the connect() our component code is separated into two parts. The first being what we will call the inner component, this component on its own is unaware that redux even exists.

The second is the glue code written by connect(), it glues redux and our inner component together through the use of a Higher Order Component.

Let's write some tests for this connect() example:

/**
 * https://github.com/Samic8/react-redux-use-selector-vs-connect/blob/master/src/features/counter-connect/CounterConnect.js
 */
import React, { useState } from "react";
import { connect } from "react-redux";
import {
  decrement,
  increment,
  incrementByAmount,
  incrementAsync,
  selectCount,
} from "../../app/counterSlice";
import styles from "./CounterConnect.module.css";

export function CounterConnectInner({
  count,
  increment,
  decrement,
  incrementByAmount,
  incrementAsync,
}) {
// ...
}

const mapStateToProps = (state) => {
  return {
    count: selectCount(state),
  };
};

const mapDispatchToProps = (dispatch) => {
  return {
    increment: () => dispatch(increment()),
    decrement: () => dispatch(decrement()),
    incrementByAmount: (amount) => dispatch(incrementByAmount(amount)),
    incrementAsync: (amount) => dispatch(incrementAsync(amount)),
  };
};

export const CounterConnect = connect(
  mapStateToProps,
  mapDispatchToProps
)(CounterConnectInner);

The inner component we can test independent of redux because of its separation. We don't have this option with Redux hooks because the glue code is within the component.

/**
 * https://github.com/Samic8/react-redux-use-selector-vs-connect/blob/master/src/features/counter-connect/CounterConnect.test.js
 */
import React from "react";
import { render, screen } from "../../test-util";
import { CounterConnectInner, CounterConnect } from "./CounterConnect";

// Testing component that is not connected to redux
describe("CounterConnectInner", () => {
  it("shows the current count", () => {
    render(<CounterConnectInner count={20} />);
    expect(screen.queryByText("20")).toBeInTheDocument();
  });
  // ...
});

Like we would do with a Redux hooks component, we can also test the inner and Redux glue code together by testing the component returned by the connect function.

/**
 * https://github.com/Samic8/react-redux-use-selector-vs-connect/blob/master/src/features/counter-connect/CounterConnect.test.js
 */
// ...
describe("CounterConnectInner", () => {
// ...
  it("shows the current count", () => {
    render(<CounterConnect />, {
      initialState: { counter: { value: 20 } },
    });
    expect(screen.getByText("20")).toBeInTheDocument();
  });
});

Note: The redux store is setup in the imported test-util which allows the insertion of the initial state for convenience.

This test is more comprehensive because it tests more lines code. Testing more lines of code is not always better, the glue code is minimal and if you want to gamble with not testing it your tests will be simpler but less complete.

Different components may be better tested with either approach.

Hooks testing

When using Redux Hooks we are nudged towards testing components that include their glue code like the second connect() example.

Using Redux hooks does not mean you can't write components that are unaware of Redux, you should do that whenever possible.

The Redux hooks version of the same component:

/**
 * https://github.com/Samic8/react-redux-use-selector-vs-connect/blob/master/src/features/counter-use-selector/CounterUseSelector.js
 */
import React, { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import {
  decrement,
  increment,
  incrementByAmount,
  incrementAsync,
  selectCount,
} from "../../app/counterSlice";
import styles from "./CounterUseSelector.module.css";

export function CounterUseSelector() {
  const count = useSelector(selectCount);
  const dispatch = useDispatch();
  // ...
}

With Redux hooks we are nudged towards writing tests that are inclusive of redux (glue code). Our tests will normally include redux state:

/**
 * https://github.com/Samic8/react-redux-use-selector-vs-connect/blob/master/src/features/counter-use-selector/CounterUseSelector.test.js
 */
import React from "react";
import { render, screen } from "../../test-util";
import { CounterUseSelector } from "./CounterUseSelector";

// Testing component that is connected to redux
describe("CounterConnect", () => {
  it("shows the current count", () => {
    render(<CounterUseSelector />, {
      initialState: { counter: { value: 20 } },
    });
    expect(screen.getByText("20")).toBeInTheDocument();
  });
});

We end up with less flexibility but it nudges us towards writing more comprehensive tests.

I am using the word nudge here again because it does not force us to test components that include Redux glue code. We could break this component into two components, one that includes Redux hooks and another that receives props. But at that point we are replicating the purpose of connect(), so we may as well use it.

Better performance optimizations by default

The connected component will re-render only when the properties returned from mapStateToProps changes.

const mapStateToProps = state => {
  const count = selectCount(state)

  return {
    count,
  }
}

export default connect(mapStateToProps)(CounterComponent)

We must be careful not to create new objects because connect uses strict equality (===) on each of the properties.

const mapStateToProps = state => {
  const count = selectCount(state)
  const history = [...selectPreviousNumbers(state), count]

  return {
    count,
    history,
  }
}

export default connect(mapStateToProps)(CounterComponent)

In this example, the history value will create a new array (arrays are objects too!) each time causing the connected component to re-render every time the state changes. If you do need to return objects reselect can help memorize and return the same reference when the data has not changed.

Components using Redux hooks can achieve the same functionality by making use of the React.memo.

function CounterUseSelector({ allowValueChange }) {
  const count = useSelector(selectCount)
  const dispatch = useDispatch()
  // ...
}

export default React.memo(CounterUseSelector)

useSelector() will cause re-renders if the value it produces has changed. When returning an object be aware that unless it's the same object by reference equality the component will re-render even if the properties of the object are the same. You can get around this by always returning the same object.

What approach truly give your app better performance is best left decided to actual testing.

Less boilerplate

Using React hooks useSelector() forgoes the need to use the connect function and embeds that logic within the components themselves. The trade-off is a reduction on the Separation of Concerns spectrum and the need to be aware of when to use React.memo to get the same performance optimizations.

Read the docs for a more in-depth understanding

This article provides a framework to compare the approaches through the theme of nudging. But to truly get an understanding of the details of the hooks read the official documentation.

The "Zombie Children" problem

The docs go into detail about a problem that can arise through Redux hook usage. The docs have a lot of information on this issue but it's hard to grasp exactly how it would affect your code. Let me know if you would like a video tutorial on this problem.

The redux style-guide now recommends using the hooks API. When this article was first written there was no official recommendation.

Conclusion

Understand what you are optimizing for and choose the method that best suits that. If you don't know what that is and just want to get started using React-Redux I recommend the hooks approach.

You might choose to not confine yourself to using the connect() or Redux hooks. If you don't make a decision upfront you might be creating more decision fatigue because it's not always clear why one method is a better approach over another. As we have seen in this article the differences are subtle.

Additional Resources

Was this article helpful?