React 18中useEffect首次页面加载时执行两次的原因排查
问题根因
这是React 18开发环境的默认行为,不是代码bug。
React 18默认在入口文件开启了<React.StrictMode>严格模式,开发环境下组件首次挂载时,会对依赖数组为空的useEffect执行「挂载→卸载→重新挂载」的模拟流程,用来提前暴露代码中未正确清理的副作用问题(比如未取消的请求、未清除的定时器、未解绑的事件监听),因此你会看到effect执行了两次。这个双调用逻辑仅在开发环境生效,生产环境打包后不会出现。
可选解决方案
推荐方案:给请求增加取消逻辑(符合React设计规范)
这是官方推荐的写法,不仅能解决严格模式下的重复请求问题,还能避免组件快速卸载/重挂载时的请求竞态问题。修改后的代码如下:
import axios from "axios"; import React, { useEffect, useState } from "react"; import toast from "react-hot-toast"; import { useParams } from "react-router-dom"; import { BASE_API_URL } from "../Variables/Urls"; const SingleRecipeView = () => { const { slug } = useParams(); const [recipe, setRecipe] = useState({}); const [loading, setLoading] = useState(true); useEffect(() => { const abortController = new AbortController(); const fetchRecipe = async () => { try { setLoading(true); const res = await axios.get(`${BASE_API_URL}/recipe/get/${slug}`, { signal: abortController.signal }); console.log(res.data); setRecipe(res.data); setLoading(false); } catch (err) { // 主动取消的请求不需要抛错提示 if (!axios.isCancel(err)) { console.log(err); setLoading(false); toast.error("Error loading recipe"); } } }; fetchRecipe(); // 组件卸载时终止未完成的请求 return () => abortController.abort(); // 注意:slug是effect内用到的外部变量,需要加入依赖数组 }, [slug]); // 其余组件渲染逻辑 }
简易方案:加请求标记控制只执行一次
如果只是想快速避免重复请求,可以用useRef存一个标记位,判断是否已经发起过请求:
import React, { useEffect, useState, useRef } from "react"; // 其余导入逻辑不变 const SingleRecipeView = () => { const { slug } = useParams(); const [recipe, setRecipe] = useState({}); const [loading, setLoading] = useState(true); const hasFetched = useRef(false); const fetchRecipe = async () => { // 原有请求逻辑保持不变 }; useEffect(() => { if (hasFetched.current) return; hasFetched.current = true; fetchRecipe(); }, [slug]); // 其余组件渲染逻辑 }
注意:这个写法无法解决路由参数快速变化时的请求竞态问题,只适合简单场景使用。
不推荐方案:关闭严格模式
可以直接删除入口文件(通常是src/main.jsx/src/index.js)中包裹根组件的<React.StrictMode>标签,关闭后开发环境也不会出现双调用。但严格模式可以提前帮你发现很多潜在的代码问题,非必要不建议关闭。
内容的提问来源于stack exchange,提问作者Debanjan Tewary
相关产品推荐
相关产品推荐

