I have 2 apps, notes app and anecdotes app. Both are React 18 apps. And I am using the Redux-toolkit.
Both apps are fairly similar. One difference is that the notes app has a reducer to toggle the importance of a note (true and false) and the anecdotes app has a vote incrementor for each anecdote (vote count incrementing). But the code is very similar for these functionality in the reducers.
Browser error that comes for the Anecdotes app:
Error: [Immer] An immer producer returned a new value and modified its draft. Either return a new value or modify the draft.
Issue: The toggleImportance
reducer in the notes app works properly without any issue. However, in the anecdotes app the map return function in the increaseVotes
reducer creates an Immer error in the browser console. I have found a solution to fix this error using void
for the return statement in the increaseVotes
reducer. See link references:
Question: I am confused as to why one app is working properly without the void
and the other app is giving me an error without the void
. I don't understand this behavior difference when very similar code is involved in both apps. Please can someone explain this.
Notes app: noteReducer.js
file
import { createSlice, current } from '@reduxjs/toolkit';
const initialState = [
{
content: 'reducer defines how redux store works',
important: true,
id: 1,
},
{
content: 'state of store can contain any data',
important: false,
id: 2,
},
];
const generateId = () => Number((Math.random() * 1000000).toFixed(0));
const noteSlice = createSlice({
name: 'notes',
initialState,
reducers: {
createNote(state, action) {
const content = action.payload;
state.push({
content,
important: false,
id: generateId(),
});
},
// REDUCER WORKS PROPERLY WITHOUT VOID
toggleImportanceOf(state, action) {
const id = action.payload;
console.log('note id: ', id);
const noteToChange = state.find(n => n.id === id);
const changedNote = {
...noteToChange,
important: !noteToChange.important
};
console.log('current(state)', current(state));
// THIS WORKS PERFECTLY
return state.map(note => note.id !== id ? note : changedNote);
}
},
});
export const { createNote, toggleImportanceOf } = noteSlice.actions;
export default noteSlice.reducer;
Anecdotes app: anecdoteReducer.js
file
import { createSlice, current } from '@reduxjs/toolkit';
const anecdotesAtStart = [
'If it hurts, do it more often',
'Adding manpower to a late software project makes it later!',
'The first 90 percent of the code accounts for the first 90 percent of the development time...The remaining 10 percent of the code accounts for the other 90 percent of the development time.',
'Any fool can write code that a computer can understand. Good programmers write code that humans can understand.',
'Premature optimization is the root of all evil.',
'Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.',
];
const getId = () => (100000 * Math.random()).toFixed(0);
const asObject = (anecdote) => {
return {
content: anecdote,
id: getId(),
votes: 0,
};
};
const initialState = anecdotesAtStart.map(asObject);
const anecdoteSlice = createSlice({
name: 'anecdotes',
initialState,
reducers: {
createAnecdote(state, action) {
const content = action.payload;
state.push({
content,
id: getId(),
votes: 0,
});
},
// REDUCER CAUSES ERROR WITHOUT VOID
increaseVotes(state, action) {
const id = action.payload;
console.log('anecdote id: ', id);
const anecdoteToChange = state.find(a => a.id === id);
const changedAnecdote = {
...anecdoteToChange,
votes: ++anecdoteToChange.votes,
};
console.log('state', state);
console.log('current(state)', current(state));
// THIS LINE CAUSES THE IMMER ERROR
// QUESTION: WHY IS THIS LINE NOT WORKING LIKE IN THE NOTES APP (SEE CODE ABOVE)?
// return state.map(anecdote => anecdote.id !== id ? anecdote : changedAnecdote);
// ERROR FIX: Added void to the return value
return void(state.map(anecdote => anecdote.id !== id ? anecdote : changedAnecdote));
}
}
});
export const { createAnecdote, increaseVotes } = anecdoteSlice.actions;
export default anecdoteSlice.reducer;
You are mutating the state and returning a new state value.
increaseVotes(state, action) {
const id = action.payload;
console.log('anecdote id: ', id);
const anecdoteToChange = state.find(a => a.id === id);
const changedAnecdote = {
...anecdoteToChange,
votes: ++anecdoteToChange.votes, // <-- the mutation
};
console.log('state', state);
console.log('current(state)', current(state));
return state.map(anecdote => anecdote.id !== id
? anecdote
: changedAnecdote
); // <-- returning a new value
}
The first example worked because you didn't mutate the current state reference, but fully shallow copied new object references.
Do one or the other, and never both.
Mutate the current state
increaseVotes(state, action) {
const id = action.payload;
const anecdoteToChange = state.find(a => a.id === id);
if (anecdoteToChange) {
anecdoteToChange.votes++;
}
}
or
Create and return new state references
increaseVotes(state, action) {
const id = action.payload;
return state.map(anecdote => anecdote.id === id
? {
...anecdote,
votes: anecdote.votes + 1,
}
: anecdote
);
}