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

React函数中setState多次执行问题排查(已更新)

问题描述

问题概述

我有一个包含meals的Context,meals在MealDetails组件中使用。该组件会渲染多个Food组件组成的列表:点击Food组件的X按钮会移除对应食物,否则跳转到FoodDetails页面。

点击按钮时,deleteFood函数会执行,通过setState重新设置meals状态来移除食物,但问题是这个函数里的setState不是执行一次,而是在当前组件中执行两次;在另外两个组件测试时,分别出现执行一次和四次的情况。

更新内容

我的addFood函数也有类似问题,这个函数在另一个组件中调用,用来向两个不同Context的状态中添加Food,但两个Context里添加的数值都翻倍了。我发现浏览器控制台里,setState内部的console.log会通过react_devtools_backend.js打印第二次,而且只有这两个有问题的函数会出现这种情况。

更新2

我在MealsDetails组件中渲染Food组件,而这个组件用到了MealsContext的meals,这会不会是问题根源?

更新3

补充说明:MealsContext里还有其他修改状态的函数,其中changeFoodWeight和addFood在同一个组件中调用,但没有异常,而且react_devtools_backend.js也没有相关日志。我附上了这个函数的代码。


MealsDetails.tsx

const MealDetails = () => {
  const navigate = useNavigate();

  let { id } = useParams();
  const { meals } = useMeals();

  const style = { "--percentage": "75%" } as React.CSSProperties;

  return (
    <div className="MealDetails">
      <header className="BarcodeScannerHeader">
        <ArrowBackIcon
          onClick={() => {
            navigate("/");
          }}
          sx={{ fontSize: 35 }}
        ></ArrowBackIcon>
        <div className="HeaderText">{meals.meals[Number(id)].name}</div>
      </header>
      <div className="MealDetailsContent">
              <span className="label">{meals.meals[Number(id)].calories} ate</span>
        <hr className="SolidMealDetails"></hr>
        {meals.meals[Number(id)] != null
          ? meals.meals[Number(id)].food.map((food: any, index: number) => <Food key={index} food={food}/>)
          : "Not loading"}
      </div>
      <Link className="Link" to={`/AddFood/${id!}`}>
        <div className="MealDetailsAddFoodButton">
          <img className="SVG" id="SVG" src={SVG} alt="+" />
        </div>
      </Link>
    </div>
  );
};

MealsContext.tsx

let initialState: MealsType = {
  calories: 0,
  carbs: 0,
  meals: [
    {
      calories: 0,
      carbs: 0,
      food: []
    }
  ],
};

const MealsProvider = ({ children }: any) => {
  const [meals, setMeals] = useState<MealsType>(initialState);

const addFood = async (
    id: number,
    addedFood: FoodType,
    selectedLocation: string,
    date: Date
  ) => {
      //Removed the fetch for readability
      console.log("func")
      setMeals((prevMeals) => {
        console.log("state");
        const updatedMeals = prevMeals;
        let foodExists = false;

        updatedMeals.calories += addedFood.kcal;
        updatedMeals.carbs += addedFood.carbs;
  
        updatedMeals.meals[id].calories += addedFood.kcal;
        updatedMeals.meals[id].carbs += addedFood.carbs;
  
        updatedMeals.meals[id].food.forEach((food : FoodType) => {
          if(food.code === addedFood.code){
            food.kcal += addedFood.kcal;
            food.carbs += addedFood.carbs;
            foodExists = true;
          }
        })

        if(!foodExists){
          updatedMeals.meals[id].food.push(addedFood);
        }
  
        return {...prevMeals,updatedMeals};
      });

      setUser(prevUser => {
        const updatedStorage = prevUser.storage;
  
        updatedStorage.map((storage : any) => {
          if(storage.location === selectedLocation){
            storage.storedFood.map((storedFood : any) => {
              if(storedFood.code === addedFood.code){
                storedFood.weight -= addedFood.weight;
              }
            })
          }
        })
  
        return {...prevUser, updatedStorage};
      })  
  };

const changeFoodWeight = async (
    id: number,
    foodDiff: any,
    selectedLocation: string,
    date: Date,
    newWeight: number
  ) => {
      //Removed the fetch for readability
      console.log("func");
      setMeals((prevMeals) => {
        console.log("state");
        const updatedMeals = prevMeals;
        updatedMeals.calories += foodDiff.kcalDiff;
        updatedMeals.carbs += foodDiff.carbsDiff;
  
        updatedMeals.meals[id].calories += foodDiff.kcalDiff;
        updatedMeals.meals[id].carbs += foodDiff.carbsDiff;

  
        updatedMeals.meals[id].food.forEach((food) => {
          if (food.code === foodDiff.code) {
            food.kcal += foodDiff.kcalDiff;
            food.carbs += foodDiff.carbsDiff;
          }
        });
  
        return {...prevMeals,updatedMeals};
      });

      setUser(prevUser => {
        const updatedStorage = prevUser.storage;
  
        updatedStorage.map((storage : any) => {
          if(storage.location === selectedLocation){
            storage.storedFood.map((storedFood : any) => {
              if(storedFood.code === foodDiff.code){
                storedFood.weight -= foodDiff.weightDiff;
              }
            })
          }
        })
  
        return {...prevUser, updatedStorage};
      })
  };

const deleteFood = (id: number, deletedFood: FoodType) => {
    setMeals((prevMeals) => {

      const updatedMeals = prevMeals;

      updatedMeals.calories -= deletedFood.kcal;
      updatedMeals.carbs -= deletedFood.carbs;

      updatedMeals.meals[id].calories -= deletedFood.kcal;
      updatedMeals.meals[id].carbs -= deletedFood.carbs;

      for(let i = 0; i < updatedMeals.meals[id].food.length; i++){
        if(updatedMeals.meals[id].food[i].code === deletedFood.code){
          updatedMeals.meals[id].food.splice(i, 1);
        }
      }

      return {...prevMeals,updatedMeals};
    });
  };
 return (
    <MealsContext.Provider
      value={{
        meals,
        deleteFood
      }}
    >
      {children}
    </MealsContext.Provider>
  );
};

const useMeals = () => useContext(MealsContext);

export { MealsProvider, useMeals };
};

Food.tsx

const Food = ({ food }: any) => {
  const location = useLocation();

  const {deleteFood} = useMeals();  

  return (
    <Link className="FoodContent Link" to={`/FoodDetails/`}  state= {{ food: food , location: location.pathname}} >
      <div className="Food">
        <div className="FoodDetail">
          <div className="FoodNameFoodDelete">
            <div className="FoodName">{food.name}</div>
            <img className="FoodDelete SVG" onClick={(e : any) => {e.preventDefault(); deleteFood(parseInt(location.pathname[location.pathname.length - 1]), food)}} src={SVG}></img>
          </div>
          <div className="FoodGram-FoodKcal">
            <div className="FoodBrand-FoodGram">
              <div className="FoodBrand">{food.band + ","}&nbsp;</div>
              <div className="FoodGram">
                {food.weight ? food.weight + " g" : 100 + " g"}{" "}
              </div>
            </div>
            <div className="FoodKcal">{food.kcal} kcal</div>
          </div>
        </div>
        <div className="FoodNutritions">
          <div className="FoodNutrition">
            <div className="FoodNutritionGrams">{Math.round(food.carbs*10)/10} g</div>
            <div className="FoodNutritionText">Carbs</div>
          </div>
          <div className="FoodNutrition">
            <div className="FoodNutritionGrams">{Math.round(food.protein*10)/10} g</div>
            <div className="FoodNutritionText">Protein</div>
          </div>
          <div className="FoodNutrition">
            <div className="FoodNutritionGrams">{Math.round(food.fat*10)/10} g</div>
            <div className="FoodNutritionText">Fat</div>
          </div>
        </div>
      </div>
    </Link>
  );
};

问题根源与修复方案

核心问题1:直接修改React状态的引用值

你的addFood、deleteFood函数里,都直接引用并修改了prevMeals(React状态的前值):

const updatedMeals = prevMeals; // 直接引用原状态对象,而非创建新副本

React状态是不可变的,直接修改prevState会破坏状态更新的不可变性原则,导致React无法正确追踪状态变化。加上开发环境默认开启的StrictMode会重复执行setState的更新函数来检测副作用,就会出现状态修改翻倍、执行多次的情况。

changeFoodWeight同样存在这个问题,只是当前场景未触发明显bug,本质仍不安全。

核心问题2:StrictMode的开发环境检测

React StrictMode在开发环境下会重复执行状态更新函数、useEffect回调等,目的是提前发现副作用问题。如果状态更新函数存在直接修改原状态的操作,就会被放大,出现多次执行的现象。

修复步骤

1. 深拷贝状态对象,保证不可变性

修改所有状态更新函数,对prevState进行深拷贝,确保不修改原始状态:

修复后的addFood函数

const addFood = async (
    id: number,
    addedFood: FoodType,
    selectedLocation: string,
    date: Date
  ) => {
      console.log("func")
      setMeals((prevMeals) => {
        console.log("state");
        // 深拷贝整个状态对象
        const updatedMeals = JSON.parse(JSON.stringify(prevMeals));
        let foodExists = false;

        updatedMeals.calories += addedFood.kcal;
        updatedMeals.carbs += addedFood.carbs;
  
        updatedMeals.meals[id].calories += addedFood.kcal;
        updatedMeals.meals[id].carbs += addedFood.carbs;
  
        updatedMeals.meals[id].food.forEach((food : FoodType) => {
          if(food.code === addedFood.code){
            food.kcal += addedFood.kcal;
            food.carbs += addedFood.carbs;
            foodExists = true;
          }
        })

        if(!foodExists){
          updatedMeals.meals[id].food.push(addedFood);
        }
  
        return updatedMeals;
      });

      setUser(prevUser => {
        // 深拷贝storage对象
        const updatedStorage = JSON.parse(JSON.stringify(prevUser.storage));
  
        updatedStorage.map((storage : any) => {
          if(storage.location === selectedLocation){
            storage.storedFood.map((storedFood : any) => {
              if(storedFood.code === addedFood.code){
                storedFood.weight -= addedFood.weight;
              }
            })
          }
        })
  
        return {...prevUser, storage: updatedStorage};
      })  
  };

修复后的deleteFood函数

const deleteFood = (id: number, deletedFood: FoodType) => {
    setMeals((prevMeals) => {
      // 深拷贝状态对象
      const updatedMeals = JSON.parse(JSON.stringify(prevMeals));

      updatedMeals.calories -= deletedFood.kcal;
      updatedMeals.carbs -= deletedFood.carbs;

      updatedMeals.meals[id].calories -= deletedFood.kcal;
      updatedMeals.meals[id].carbs -= deletedFood.carbs;

      // 用filter替代splice,更符合不可变原则
      updatedMeals.meals[id].food = updatedMeals.meals[id].food.filter(food => food.code !== deletedFood.code);

      return updatedMeals;
    });
  };

修复后的changeFoodWeight函数

const changeFoodWeight = async (
    id: number,
    foodDiff: any,
    selectedLocation: string,
    date: Date,
    newWeight: number
  ) => {
      console.log("func");
      setMeals((prevMeals) => {
        console.log("state");
        const updatedMeals = JSON.parse(JSON.stringify(prevMeals));
        updatedMeals.calories += foodDiff.kcalDiff;
        updatedMeals.carbs += foodDiff.carbsDiff;
  
        updatedMeals.meals[id].calories += foodDiff.kcalDiff;
        updatedMeals.meals[id].carbs += foodDiff.carbsDiff;
  
        updatedMeals.meals[id].food.forEach((food) => {
          if (food.code === foodDiff.code) {
            food.kcal += foodDiff.kcalDiff;
            food.carbs += foodDiff.carbsDiff;
          }
        });
  
        return updatedMeals;
      });

      setUser(prevUser => {
        const updatedStorage = JSON.parse(JSON.stringify(prevUser.storage));
  
        updatedStorage.map((storage : any) => {
          if(storage.location === selectedLocation){
            storage.storedFood.map((storedFood : any) => {
              if(storedFood.code === foodDiff.code){
                storedFood.weight -= foodDiff.weightDiff;
              }
            })
          }
        })
  
        return {...prevUser, storage: updatedStorage};
      })
  };

2. 优化组件key值(推荐)

MealDetails中渲染Food组件时用index作为key,当数组元素删除时会导致React复用错误组件,建议改用食物的唯一标识:

meals.meals[Number(id)].food.map((food: any) => <Food key={food.code} food={food}/>)

3. 关于StrictMode(可选)

开发环境的StrictMode是用来检测潜在问题的,生产环境不会有重复执行的情况,不建议移除。如果想临时屏蔽日志,可暂时去掉根组件的<StrictMode>包裹。

内容的提问来源于stack exchange,提问作者Cinc-B

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 17:40:25