I’m trying to learn how to use Redux toolkit using createSlice() to display my next bookable data. When I used sort() function on the local variable bookable (not a state) in BookableList.js, it raised an error:
A state mutation was detected between dispatches, in the path
'bookables.0.days.0'
.
bookable.days.sort().map((d, i) => (
<li key={i}>{days[d]}</li>
))}
Here’s my code:
// BookableList.js
import React, { Fragment } from 'react';
import { sessions, days } from '../../static.json';
import { useSelector, useDispatch } from 'react-redux';
import { getNextBookable } from './BookablesSlice';
import { FaArrowRight } from 'react-icons/fa';
export const BookablesList = () => {
const dispatch = useDispatch();
const { group, bookableIndex, hasDetails, bookables } = useSelector( state => state );
const bookablesInGroup = bookables.filter((b) => b.group === group);
const groups = [...new Set(bookables.map((b) => b.group))];
const bookable = bookablesInGroup[bookableIndex];
const nextBookableHandler = () => {
dispatch(getNextBookable());
};
// ...
return (
<Fragment>
<div>
<select onChange={changeGroup}>
{groups.map((name, i) => (
<option key={i}>{name}</option>
))}
</select>
<ul className='bookables items-list-nav'>
{bookablesInGroup.map((b, i) => (
<li key={i} className={i === bookableIndex ? "selected" : null}>
<button className='btn' onClick={changeBookable}>
{b.title}
</button>
</li>
))}
</ul>
<p>
<button
className='btn'
onClick={nextBookableHandler}
autoFocus
>
<FaArrowRight>
<span>Next</span>
</FaArrowRight>
</button>
</p>
</div>
{bookable && (
<div className='bookable-details'>
<div className='item'>
<div className='item-header'>
<h2>{bookable.title}</h2>
<span className='controls'>
<input
type='checkbox'
id='hasDetails'
name='hasDetails'
checked={hasDetails}
onChange={toggleHasDetails}
/>
<label htmlFor='showDetails'>Show Details</label>
</span>
</div>
<p>{bookable.notes}</p>
{hasDetails && (
<div className='item-details'>
<h3>Availability</h3>
<div className='bookable-availability'>
<ul>
{
// Replaced sort() with toSorted() function to avoid a mutation during render.
bookable.days.sort().map((d, i) => (
<li key={i}>{days[d]}</li>
))}
</ul>
<ul>
{bookable.sessions.toSorted().map((s, i) => (
<li key={i}>{sessions[s]}</li>
))}
</ul>
</div>
</div>
)}
</div>
</div>
)}
</Fragment>
);
};
// BookablesSlice.js
import { createSlice } from '@reduxjs/toolkit';
export const bookablesSlice = createSlice({
name: 'nextBookable',
initialState: {},
reducers: {
getNextBookable: (state, action) => {
const count = state.bookables.filter(b => b.group === state.group).length;
return {
...state,
bookableIndex: (state.bookableIndex + 1) % count,
}
},
}
})
// this is for dispatch
export const { getNextBookable } = bookablesSlice.actions;
//this is for configureStore
export default bookablesSlice.reducer;
//store.js
import { configureStore } from '@reduxjs/toolkit';
import bookableReducer from './components/Bookables/BookablesSlice';
import { bookables } from './static.json';
const initialState = {
group: "Rooms",
bookableIndex: 0,
hasDetails: true,
bookables,
};
const store = configureStore({
reducer: bookableReducer,
preloadedState: initialState,
});
export default store;
I replaced the sort()
with toSorted()
which fixed the problem but I don’t understand why – because the mutation happened on a local variable not on the state data itself.
2
Answers
.sort
modifies your array – and yourbookable
is just a pointer to the store. Selectors do not create a clone of your store, they return the store value itself – so you did modify your store outside of a reducer here.Array.prototype.sort
does an in-place mutation of the array it operates over.bookables
is a reference to the object stored in state, and mutating it outside a reducer in the UI code will cause this invariant check to fail.Creating a shallow copy of the array resolves this because you are now mutating the new array reference.
See Array.prototype.toSorted, emphasis mine:
The previous method was usually to do an inline shallow copy of the array first, then sort it.