A state mutation was detected between dispatches, Redux toolkit and selectors/reselect

Viewed 342

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.

2 Answers

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!

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),
);
Related