React useReducer每次dispatch执行两次问题求助
React useReducer 重复触发dispatch导致数据异常问题
问题情况
- useReducer的每一次dispatch都会执行两次,已尝试将useReducer移出组件、关闭Strict Mode,问题依然存在
- 重复执行导致本该生成的空数组
[]被错误生成为[''] - 复现方式:在顶部第一个输入框输入数字,点击旁边按钮会出现红色恢复按钮,点击后控制台会打印两次日志,所有类型的dispatch都存在该问题
原reducer代码
function reducer(state, action) { const arrayPueblosYexclus = ["pueblos_T1","pueblos_T2","pueblos_T3","exclusiones"]; switch (action.type) { case "RESTORE": console.log("restored"); return valorPrevio; break; case "FETCH_SUCCESS": const datos = action.payload; datos?.forEach((element) => { arrayPueblosYexclus?.map(arel=>{ return element[arel] !== null ? (element[arel] = element?.[arel] ?.toString() .trim() .split(",")) : (element[arel] = []); }) console.log(element.pueblos_T1,element.pueblos_T2,element.pueblos_T3); }); setLoading(false); setValorPrevio(state); return { ...state, data: datos, loading: false, error: null }; break; case "FETCH_ERROR": return { ...state, data: [], loading: false, error: Number(action.payload).toFixed(2), }; break; case "UPDATE_DATA": return { ...state, data: state.data.map((item) => { if (item.id === action.id) { return { ...item, [action.property]: action.payload }; } return item; }), }; break; case "DELETE_FROM_VILLAGES": return { ...state, data: state.data.map((item) => { if (item.id === action.municipio_id) { return { ...item, [action.grupo]: item[action.grupo].filter( (villa) => villa !== action.barrio ), }; } return item; }), }; break; case "RISE_PRICES": setValorPrevio(state); return { ...state, data: state?.data.map((item) => { const updatedItem = { ...item }; const propertiesToUpdate = [ "precioT1_poco", "precioT1_normal", "precioT1_grua", "precioT2_poco", "precioT2_normal", "precioT2_grua", "precioT3_poco", "precioT3_normal", "precioT3_grua", ]; propertiesToUpdate.forEach((property) => { if ( item[property] && item[property] !== 0 && item[property] !== "0" ) { if (action.operation_type === "porcentual") { updatedItem[property] = ( Number(item[property]) + (Number(item[property]) * action.vlue) / 100 ) .toFixed(2) .replace(/\.00$/, ""); setPercentage(""); } if (action.operation_type === "normal") { updatedItem[property] = ( Number(item[property]) + Number(action.vlue) ) .toFixed(2) .replace(/\.00$/, ""); setNormal(""); } } }); return updatedItem; }), }; break; case "ADD_VILLAGE": let villagesArray = action.villages.split(",").map(v=>v.trim().charAt(0).toUpperCase() + v.trim().slice(1)); return { ...state, data: state.data.map((item,i) => { if (item.id === action.selected_municipio) { let villagescoincidence = villagesArray.filter(v => item[action.T].includes(v)); villagesArray = villagesArray.filter(v => !item[action.T].includes(v)); if (villagescoincidence?.length > 0) { let coincidentes = villagescoincidence.join(", "); console.log(`This message is appearing twice`); } return { ...item, [action.T]: [...(item[action.T].filter(e => e !== "") || []), ...villagesArray], }; } return item; }), }; break; default: return state; } }
问题原因
- Reducer违反纯函数原则:reducer内部直接调用
setLoading、setValorPrevio等状态更新函数,属于副作用操作。Reducer必须是纯函数,不能修改外部状态、执行异步操作或直接修改入参,否则会触发额外的状态更新,导致重复执行。 - 直接修改原始数据:
FETCH_SUCCESS中直接修改datos(即action.payload)的元素属性,污染了原始数据,引发不必要的状态更新。 - 依赖外部状态变量:
valorPrevio是组件外部的状态,在reducer中直接使用会让状态更新逻辑依赖外部环境,破坏reducer的纯性,导致不可预测的执行。
修复方案
1. 移除Reducer中的所有副作用
把setLoading、setValorPrevio、setPercentage、setNormal移到组件的事件处理或useEffect中:
setLoading(false)放在发起FETCH_SUCCESSdispatch的逻辑里setValorPrevio用useEffect监听state变化,每次状态更新后保存当前状态setPercentage和setNormal移到触发RISE_PRICES的按钮点击事件中
2. 避免修改原始数据
在FETCH_SUCCESS中创建新对象处理数据,不修改原始的action.payload:
// 替换原forEach逻辑 const processedDatos = datos?.map(element => { const newElement = {...element}; arrayPueblosYexclus?.forEach(arel => { newElement[arel] = newElement[arel] !== null ? newElement[arel]?.toString().trim().split(",") : []; }); return newElement; });
3. 将外部状态整合到Reducer的State中
把valorPrevio作为state的一个字段(如previousState),在需要保存状态时(如RISE_PRICES)存入当前state,RESTORE时直接返回state.previousState:
// RESTORE分支 case "RESTORE": console.log("restored"); return state.previousState || state; // RISE_PRICES分支末尾 return { ...state, data: updatedData, previousState: state // 保存当前状态 };
4. 修正数组处理逻辑,避免空字符串
在ADD_VILLAGE中提前过滤空字符串,避免重复执行生成['']:
let villagesArray = action.villages.split(",") .map(v => v.trim()) .filter(v => v !== "") .map(v => v.charAt(0).toUpperCase() + v.slice(1)); // 合并数组时也过滤空值 [...item[action.T].filter(e => e.trim() !== ""), ...newVillages]
修复后的Reducer示例
function reducer(state, action) { const arrayPueblosYexclus = ["pueblos_T1","pueblos_T2","pueblos_T3","exclusiones"]; switch (action.type) { case "RESTORE": console.log("restored"); return state.previousState || state; case "FETCH_SUCCESS": const processedDatos = action.payload?.map(element => { const newElement = {...element}; arrayPueblosYexclus?.forEach(arel => { newElement[arel] = newElement[arel] !== null ? newElement[arel]?.toString().trim().split(",") : []; }); return newElement; }); return { ...state, data: processedDatos, loading: false, error: null, previousState: state }; case "FETCH_ERROR": return { ...state, data: [], loading: false, error: Number(action.payload).toFixed(2), }; case "UPDATE_DATA": return { ...state, data: state.data.map((item) => { if (item.id === action.id) { return { ...item, [action.property]: action.payload }; } return item; }), }; case "DELETE_FROM_VILLAGES": return { ...state, data: state.data.map((item) => { if (item.id === action.municipio_id) { return { ...item, [action.grupo]: item[action.grupo].filter( (villa) => villa !== action.barrio ), }; } return item; }), }; case "RISE_PRICES": const updatedData = state?.data.map((item) => { const updatedItem = { ...item }; const propertiesToUpdate = [ "precioT1_poco", "precioT1_normal", "precioT1_grua", "precioT2_poco", "precioT2_normal", "precioT2_grua", "precioT3_poco", "precioT3_normal", "precioT3_grua", ]; propertiesToUpdate.forEach((property) => { if ( item[property] && item[property] !== 0 && item[property] !== "0" ) { if (action.operation_type === "porcentual") { updatedItem[property] = ( Number(item[property]) + (Number(item[property]) * action.value) / 100 ) .toFixed(2) .replace(/\.00$/, ""); } else if (action.operation_type === "normal") { updatedItem[property] = ( Number(item[property]) + Number(action.value) ) .toFixed(2) .replace(/\.00$/, ""); } } }); return updatedItem; }); return { ...state, data: updatedData, previousState: state }; case "ADD_VILLAGE": let villagesArray = action.villages.split(",") .map(v => v.trim()) .filter(v => v !== "") .map(v => v.charAt(0).toUpperCase() + v.slice(1)); return { ...state, data: state.data.map((item) => { if (item.id === action.selected_municipio) { const existingVillages = item[action.T].filter(e => e.trim() !== ""); const newVillages = villagesArray.filter(v => !existingVillages.includes(v)); const coincidentes = villagesArray.filter(v => existingVillages.includes(v)); if (coincidentes.length > 0) { console.log(`已存在村庄:${coincidentes.join(", ")}`); } return { ...item, [action.T]: [...existingVillages, ...newVillages], }; } return item; }), }; default: return state; } }
内容的提问来源于stack exchange,提问作者Germán
相关产品推荐
相关产品推荐

