reselect icon indicating copy to clipboard operation
reselect copied to clipboard

Issue while testing component that uses createSelector

Open petermarcoen opened this issue 4 years ago • 1 comments

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 createSelector and just use useSelector it 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.

petermarcoen avatar Feb 16 '21 11:02 petermarcoen

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.

yuningjiang123 avatar Jul 06 '22 08:07 yuningjiang123