React useReducer处理函数为何是不纯函数?StrictMode交换异常排查
React StrictMode下useReducer两次调用导致交换异常的问题解决
你的问题核心是reducer函数不是纯函数,浅拷贝后直接修改了原状态的数组引用,导致StrictMode两次调用时,第二次的状态已经被第一次修改,最终触发两次交换,让元素跳过一个位置。
问题代码的关键缺陷
let modified_song = {...song} // ...后续直接修改modified_song.song.songsheet.aSections的元素
{...song}只是浅拷贝,modified_song.song.songsheet.aSections仍然和原状态的数组指向同一个引用。当你修改这个数组的元素时,实际上是在直接修改原状态中的数组——这违反了reducer必须是纯函数的要求:纯函数不能修改输入参数,必须返回全新的状态,且相同输入必须得到相同输出。
在StrictMode的开发环境下,React会重复调用reducer来检测不纯的副作用。第一次调用时,原数组已经被修改;第二次调用时,reducer拿到的song已经是第一次交换后的状态,相当于连续执行了两次交换操作,自然会让元素跳过一个位置(比如原位置i,第一次和i+1交换,第二次又和i+2交换)。
修复方案:创建全新的数组副本
要保证reducer的纯函数特性,必须对aSections创建新的数组副本,在新数组上完成交换操作,而不是修改原引用的数组。修改后的代码如下:
case 'swap_section': { // 逐层拷贝到aSections层级,确保每个引用都是新的 const modified_song = { ...song, song: { ...song.song, songsheet: { ...song.song.songsheet, aSections: [...song.song.songsheet.aSections] // 创建数组的新副本 } } }; const target_section_index = modified_song.song.songsheet.aSections.findIndex( section => action.daw === section.daw ); const swap_section_index = action.direction === "down" ? target_section_index + 1 : target_section_index - 1; // 检查交换索引的有效性 if (swap_section_index > -1 && swap_section_index < modified_song.song.songsheet.aSections.length) { // 在新数组上执行交换 const temp = modified_song.song.songsheet.aSections[target_section_index]; modified_song.song.songsheet.aSections[target_section_index] = modified_song.song.songsheet.aSections[swap_section_index]; modified_song.song.songsheet.aSections[swap_section_index] = temp; } return modified_song; }
修复逻辑说明
- 通过逐层拷贝,我们创建了
aSections的全新数组副本,修改这个新数组不会影响原状态,确保每次调用reducer时,输入的song都是未被修改的初始状态。 - 即使StrictMode两次调用reducer,两次执行都是基于相同的初始状态,最终React会忽略第二次的执行结果,只保留第一次的有效交换,不会出现元素跳过的问题。
额外提醒
reducer必须始终是纯函数:
- 不能修改传入的
state参数 - 不能执行有副作用的操作(比如异步请求、DOM修改、
Math.random()等) - 相同的输入必须返回完全相同的输出
这样才能保证React状态的可预测性,同时让StrictMode的检测机制正常工作。
内容的提问来源于stack exchange,提问作者user18693377
相关产品推荐
相关产品推荐

