reselect
reselect copied to clipboard
Issue while testing component that uses createSelector
I have a weird issue which is causing some of my tests to fail. I am not sure if this is a problem with reselect or with redux-mock-store (or with me)
I created a simplified version of my problem on CodeSandbox: https://codesandbox.io/s/wizardly-wave-nkuvw?file=/src/Component.js
Here is the problem:
I have a component (Component.js) which uses createSelector to get a value from the store:
const itemSelector = (state) => state.items;
const itemForId = createSelector(
[itemSelector, (state, id) => id],
(items, id) => items.find((item) => item.id === id)
);
export default function Component() {
const item = useSelector((state) => itemForId(state, 1));
return <div>{item.value}</div>;
}
To test this component I created 2 different tests (Component.test.js) in which I use react-mock-store to mock my store.
-
Case 1: Mock item with ID 1 and value 1
-
Case 2: Mock item with ID 1 and value 2
When I create a store by calling mockStore with these items directly everything works as expected.
it("Case 1 - renders value 1 for item 1 when offering new object to mockStore", async () => {
const store = mockStore({
items: [
{
id: 1,
value: "Value 1"
}
]
});
render(
<Provider store={store}>
<Component />
</Provider>
);
expect(await screen.findByText("Value 1"));
});
it("Case 2 - renders value 2 for item 1 when offering new object to mockStore", async () => {
const store = mockStore({
items: [
{
id: 1,
value: "Value 2"
}
]
});
render(
<Provider store={store}>
<Component />
</Provider>
);
expect(await screen.findByText("Value 2"));
});
Case 1 returns value 1 and Case 2 returns value 2 👍
Now, in my real app I have other parts of my state that I have to mock so I start from a common object (initialState) and append these items to it. I recreated that here as Case 3 and 4:
const initialState = {
items: []
};
it("Case 3 - renders value 1 for item 1 when offering copied object to mockStore", async () => {
let state = initialState;
state.items = [
{
id: 1,
value: "Value 1"
}
];
const store = mockStore(state);
render(
<Provider store={store}>
<Component />
</Provider>
);
expect(await screen.findByText("Value 1"));
});
it("Case 4 - renders value 2 for item 1 when offering copied object to mockStore", async () => {
let state = initialState;
state.items = [
{
id: 1,
value: "Value 2"
}
];
const store = mockStore(state);
render(
<Provider store={store}>
<Component />
</Provider>
);
expect(await screen.findByText("Value 2"));
});
Now Case 4 fails because the selector returns the value from Case 3 (value 1). Some observations:
- running only the last test does succeed
- initialState is const so it should not be able to carry over data from Case 3 to Case 4
- if I print out the state of the failing test before creating the mockStore it show the correct item (value 2)
- if I skip
createSelectorand just useuseSelectorit does work correctly:
const item = useSelector((state) =>
state.items.find((item) => item.id === 1)
);
This last observation leads me to believe this might be an issue with Reselect.
I would appreciate some help. Thank you.
I think it is because, under the hood, reselect will check the equality of your input arguments first, before checking the extracted output arguments.
In Case 1 and 2, the references of the initialState passed into mockStore are different, so the references of state in your useSelector are different, itemForId will recalculate a result based on the differenct state.
In Case 3 and 4, the references of the initialState are the same, therefore itemForId will use the result cached in Case 3 when running in Case 4.
You won't hit such problem if you're using redux because each time you dispatch an action, a root state with a new reference will give birth under the hood.
Your other observations will make sense based on the theory above.
I'm poor at English. Let me know if I have made mistakes.