3

I'm having some trouble regarding createSelector and async thunk.

Basically I'm trying to retrieve a list of links, and sort them depending on how many clicks they have, from highest to lowest.

Here is my simplified code.

MainPage.tsx

export const MainPage = (): JSX.Element => {
  const dispatch = useDispatch();
  const allLinks = useSelector(selectLinks);
  
  useEffect(() => {
    dispatch(getLinks());
  }, [dispatch]);

  return (
    <div>
      {allLinks.map((link) => ()} //unrelated logic
    </div>
  );
};

links.actions.ts

export const getLinks = createAsyncThunk(
  "getLinks",
  async () => {
    const links = await axios.get"/links")
    return links;
  },
);

links.slice.ts

const linksSlice = createSlice({
  name: "data",
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder
    .addCase(
      getLinks.fulfilled,
      (state, action) => {
        state.links = action.payload;
      },
    );
  },
});

links.selectors.ts

const selectLinksState = (state: RootState) => state.links;

export const selectLinks = createSelector(selectLinksState, (state) =>
  state.links.sort((a, b) => b.clickCount - a.clickCount),
);

So this compiles fine, but when I open it in the web browser, I get the following error Error: Invariant failed: A state mutation was detected between dispatches

Does anyone know what I'm doing wrong? I'm pretty sure it's something to do with the selectors because if I sort the links directly after fetching them in the action, it works fine.

ffx292
  • 542
  • 12
  • 27

2 Answers2

4

Sort uses an in-place sorting algorithm, it doesn't return a new array, so you are mutating the state via the selector logic.

Array.prototype.sort

The sort() method sorts the elements of an array in place and returns the sorted array. The default sort order is ascending, built upon converting the elements into strings, then comparing their sequences of UTF-16 code units values.

You can create a copy of the links array by using .slice in-line and then sort the new array reference:

export const selectLinks = createSelector(
  [selectLinksState],
  (state) => state.links.slice().sort((a, b) => b.clickCount - a.clickCount),
);
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • Thanks, I managed to figure out the cause a few minutes ago. Thanks for reaffirming ! – ffx292 Aug 19 '21 at 04:02
  • @ffx292 Yeah, I was most of the way through typing up an answer and saw another answer pop up and wanted to ensure mine was sufficiently different before submitting. Didn't notice it was a self-answer. Cheers. – Drew Reese Aug 19 '21 at 04:05
3

So even though Redux toolkit is mutable, I tinkered around with the reselect function and changed it to so:

export const selectLinks = createSelector(selectLinksState, (state) => {
  const sortedArray = [...state.links].sort(
    (a, b) => b.clickCount - a.clickCount,
  );
  return sortedArray;
});

My logic was to create a new array, so that it wouldn't cause any mutation problems. It works fine now!

ffx292
  • 542
  • 12
  • 27