Oct 20 2021
The useSelector Anti-Pattern
There are two common ways of accessing a redux store: either by connect
or useSelector
.
I will discuss why I prefer connect
and why you should too.
Simply put, useSelector
makes life unnecessarily difficult due to coupling.
Consider a user profile page that displays data from the redux store.
// UserProfilePage.tsx
import React from "react";
import { useSelector } from "react-redux";
import type { State } from "./Store";
export function UserProfilePage() {
const user = useSelector((state: State) => state.user);
return (
<div>
Name: <span data-testid='name'>{user.firstName} {user.lastName}</span>
<br />
Occupation: <span data-testid='occupation'>{user.occupation}</span>
<br />
Hobbies: <span data-testid='hobbies'>{user.hobbies.join(', ')}</span>
<br />
</div>
);
}
It’s pretty simple:
- Fetch the user data
- Display the user information
Woah, hold on a minute. Aren’t there two concerns here? We fetch the user information in one step, and in another we display the information. Notice that by using hooks, this coupling is inescapable.
Though this may seem pedantic at the surface, let’s explore this thought a little further.
// UserProfilePage.tsx
import React from "react";
import { useFetchUser } from "./useFetchUser";
export function UserProfilePage() {
const user = useFetchUser(); // <-- Changed hook call
return (
<div>
Name: <span data-testid='name'>{user.firstName} {user.lastName}</span>
<br />
Occupation: <span data-testid='occupation'>{user.occupation}</span>
<br />
Hobbies: <span data-testid='hobbies'>{user.hobbies.join(', ')}</span>
<br />
</div>
);
}
I’m not sure how you feel about this, but this makes me very uncomfortable. Whether we are fetching this data from the redux store or from an API, we should not tie the data retrieval and display together. Furthermore, writing a unit test for UserProfilePage becomes difficult too.
// UserProfile.test.tsx
import { createStore } from "redux";
import { Provider } from "react-redux";
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import { UserProfilePage } from "./UserProfilePage";
const initialState = {
user: {
firstName: "Jake",
lastName: "Robers",
occupation: "Software Engineer",
hobbies: ["Brewing Beer"],
},
};
describe("UserProfile", () => {
test("renders with the user's name", async () => {
const store = createStore((state) => state, initialState);
render(
<Provider store={store}>
<UserProfilePage />
</Provider>
);
expect(screen.getByTestId("name")).toHaveTextContent("Jake Robers");
});
});
Take one more look at the test description:
renders with the user’s name
So if we are checking if “Jake Robers” appears in the component, why on earth are we constructing an entire redux store? Doesn’t this seem pretty unnecessary? I sure think so. Let’s have another try at the implementation.
// UserProfilePage.tsx
import React from "react";
import { connect } from "react-redux";
import type { User } from "./Store";
export const UserProfilePage = ({ user }: { user: User }) => (
<div>
Name:{" "}
<span data-testid="name">
{user.firstName} {user.lastName}
</span>
<br />
Occupation: <span data-testid="occupation">{user.occupation}</span>
<br />
Hobbies: <span data-testid="hobbies">{user.hobbies.join(", ")}</span>
<br />
</div>
);
export default connect((state) => ({ user: state.user }))(UserProfilePage);
This is much better because our two responsibilities are split into two components:
- UserProfilePage component
- The connected component returned from react-redux
The good news doesn’t end here. Look at how clean our unit test becomes!
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import { UserProfilePage } from "./UserProfilePage";
const user = {
firstName: "Jake",
lastName: "Robers",
occupation: "Software Engineer",
hobbies: ["Brewing Beer"],
};
describe("UserProfile", () => {
test("renders with the user's name", async () => {
render(<UserProfilePage user={user} />);
expect(screen.getByTestId("name")).toHaveTextContent("Jake Robers");
});
});
The redux store is now completely yanked out. This makes sense because we never intended to test the redux store. Recall that the purpose of the test is to check the presence of the name. If more coverage is needed, I’d begin with testing the state selectors by abstracting user selection into a separate function. If even more coverage is needed, only then I would reach for constructing the redux store and render a connected component. Often this is overkill though. By the time you need connected component tests, you probably already have Selenium or Cypress running.
My conjecture: All hooks that reach for a context is harmful.
There’s still a place for hooks though. Most of the core React hooks manage local state – this is the way it should be. Remember that hooks are the shiny new toy in React, and that the jury is still out on “best practices”[1]. Use with caution.
I’m not the only one with this hot-take. There was a Github issue filed a while back expressing similar concern.
[1] I’m not a huge fan of the term “best pratices” in the software world because it leads to the notion that there’s only one right solution. This is harmful in that development teams will stop all further analysis and not correctly determine the benefits and drawbacks. Whether or not my opinion in this article is considered “best practice” is up to you. Look into the benefits and drawbacks to come up with your own conclusion.