You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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;
    }
  }

问题原因

  1. Reducer违反纯函数原则:reducer内部直接调用setLoading、setValorPrevio等状态更新函数,属于副作用操作。Reducer必须是纯函数,不能修改外部状态、执行异步操作或直接修改入参,否则会触发额外的状态更新,导致重复执行。
  2. 直接修改原始数据:FETCH_SUCCESS中直接修改datos(即action.payload)的元素属性,污染了原始数据,引发不必要的状态更新。
  3. 依赖外部状态变量:valorPrevio是组件外部的状态,在reducer中直接使用会让状态更新逻辑依赖外部环境,破坏reducer的纯性,导致不可预测的执行。

修复方案

1. 移除Reducer中的所有副作用

把setLoading、setValorPrevio、setPercentage、setNormal移到组件的事件处理或useEffect中:

  • setLoading(false)放在发起FETCH_SUCCESS dispatch的逻辑里
  • 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 21:25:22