React中useEffect钩子非预期不执行的问题排查与解决
问题根因
你遇到的useEffect不触发的现象,和react-router的SPA导航机制直接相关:
- react-router的客户端路由跳转不会触发浏览器整页刷新,如果跳转后匹配到的路由组件是同一个,只是路由参数、传入的props发生变化,React会直接复用已挂载的
Blogs组件实例,不会执行组件的重新挂载流程 - 你当前给
useEffect传的依赖数组是空值[],按照React的运行规则,这种写法的副作用只会在组件第一次挂载时执行一次,哪怕后续组件重新渲染、传入的props.ids发生变化,这个副作用都不会再次运行 - 代码本身还存在一个隐蔽的状态更新bug:你在循环内的异步请求回调里直接写
setLinks([...links, 新内容]),受JS闭包的缓存特性影响,这里拿到的links永远是组件初始化时的空值,等多个异步请求陆续返回时,每个setState都会覆盖其他请求的结果,哪怕useEffect正常执行,最终渲染的链接列表也会不全。
修复方案
- 补全
useEffect的依赖数组:把props.ids加入依赖列表,只要传入的id集合发生变化,就重新触发博客列表的拉取逻辑 - 修复异步状态的闭包问题:更新
links状态时改用函数式更新,基于上一次的状态值拼接新内容,避免多请求并发时的结果覆盖 - 增加副作用清理逻辑:组件卸载时取消后续的状态更新,避免无效的内存占用
- 给动态生成的
<Link>元素加上唯一的key属性,减少React渲染时的不必要重绘
修正后的参考实现
function Blogs(props) { const [links, setLinks] = useState([]); async function getBlog(id) { let data = await fetch(`${root}/api/blog/${id}`); data = await data.json(); return data.blog; } useEffect(() => { let isComponentMounted = true; // 拉取新列表前清空旧的链接内容 setLinks([]); props.ids.forEach(id => { getBlog(id).then(blog => { // 组件已卸载就不再执行状态更新 if (!isComponentMounted) return; setLinks(prevLinks => [ ...prevLinks, <Link key={id} to={`/blog/${id}`}>{blog.name}</Link> ]) }) }) // 组件卸载时执行清理 return () => { isComponentMounted = false; } // 将ids作为依赖项,ids变化就重新执行拉取逻辑 }, [props.ids]); return ( <div className="profile-blogs"> <div className="a">Блоги</div> <div className="b"> {links} </div> </div> ); }
注意事项
如果传入的props.ids是父组件每次渲染都新生成的数组字面量(比如父组件直接写ids={[1,2,3]}),因为数组的引用地址每次都会变化,useEffect可能会被无意义地反复触发。这种情况可以在父组件用useMemo固定ids的引用,避免不必要的重复网络请求。
内容的提问来源于stack exchange,提问作者kertAW
相关产品推荐
相关产品推荐

