skip to Main Content

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


  1. .sort modifies your array – and your bookable 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.

    Login or Signup to reply.
  2. 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 toSorted() method of Array instances is the copying version of the
    sort() method. It returns a new array with the elements sorted in
    ascending order.

    bookable.days.toSorted().map((d, i) => (
      <li key={i}>{days[d]}</li>
    ))}
    

    The previous method was usually to do an inline shallow copy of the array first, then sort it.

    bookable.days.slice().sort().map((d, i) => (
      <li key={i}>{days[d]}</li>
    ))}
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search