Redux Toolkit:为何两个相似Reducer一个触发Immer错误?
问题描述
我基于React 18和Redux Toolkit开发了两个应用:笔记应用和趣闻应用,二者核心Reducer逻辑相近。笔记应用的toggleImportanceOf reducer负责切换笔记的重要性,趣闻应用的increaseVotes reducer负责增加趣闻的投票数。但后者在未添加void时会触发Immer错误:
An immer producer returned a new value and modified its draft. Either return a new value or modify the draft
而前者可以正常运行。我已通过在return语句添加void解决错误,但想了解相似代码出现不同表现的原因。
笔记应用的noteReducer.js代码
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无需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)); // 此处运行完全正常 return state.map(note => note.id !== id ? note : changedNote); } }, }); export const { createNote, toggleImportanceOf } = noteSlice.actions; export default noteSlice.reducer;
趣闻应用的anecdoteReducer.js代码
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在未加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)); // 此行触发Immer错误 // 疑问:为什么不能像笔记应用的代码一样运行? // return state.map(anecdote => anecdote.id !== id ? anecdote : changedAnecdote); // 错误修复:在return值前添加void return void(state.map(anecdote => anecdote.id !== id ? anecdote : changedAnecdote)); } } }); export const { createAnecdote, increaseVotes } = anecdoteSlice.actions; export default anecdoteSlice.reducer;
原因分析
核心差异在于是否直接修改了Immer的draft对象:
笔记应用的Reducer:
在创建changedNote时,!noteToChange.important只是读取了draft对象的属性值并反转,没有修改原draft对象的属性。后续通过return state.map(...)返回新数组,全程只做了返回新值的操作,符合Immer的规则(要么修改draft,要么返回新值),所以不会报错。趣闻应用的Reducer:
创建changedAnecdote时,++anecdoteToChange.votes是直接对draft对象的votes属性执行了自增操作,也就是修改了draft。同时你又通过return state.map(...)返回了新数组,这就同时触发了“修改draft”和“返回新值”两种操作,违反了Immer的核心规则,因此抛出错误。
正确的修复方式
添加void只是绕过错误的权宜之计,推荐两种符合Redux Toolkit最佳实践的修复方式:
方式一:直接修改draft(推荐)
利用Immer的特性直接修改draft对象,无需返回新值:
increaseVotes(state, action) { const id = action.payload; const anecdoteToChange = state.find(a => a.id === id); if (anecdoteToChange) { anecdoteToChange.votes += 1; // 直接修改draft,无需返回新数组 } }
方式二:返回新值,不修改draft
保持纯函数风格,创建新对象和新数组,不触动原draft:
increaseVotes(state, action) { const id = action.payload; return state.map(anecdote => anecdote.id === id ? { ...anecdote, votes: anecdote.votes + 1 } // 基于原对象创建新对象,不修改draft : anecdote ); }
内容的提问来源于stack exchange,提问作者qqq

