为何useEffect清理函数无法获取最新state值?React状态管理解惑
我本学期正在学习React,对相关概念完全陌生。我编写了一段代码,用于获取用户头像并通过createObjectURL展示,同时在页面跳转时释放该URL。问题在于,尽管网站上头像已成功展示,但在useEffect的清理函数中,avatarURL并未保存获取到的头像URL(在调用revokeObjectURL前打印验证过)。
我尝试创建按钮打印URL,结果符合预期。我已了解state更新是异步的,且确认在调用清理函数(页面跳转)前state已成功更新并正确渲染头像,但清理函数中仍获取到旧值。我想了解:React是如何管理状态的?为何同一个“变量”在不同调用中会有不同的值?
代码示例
// State declaration const [avatarURL, setAvatarURL] = useState<string>(userDummyPFP); // Fetching API call function fillPageData() { fetch("http://localhost:3000/get-user-pfp", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ userID: "UID-2", }), credentials: "include", }) .then((response) => response.blob()) .then((data) => { const img = URL.createObjectURL(data); setAvatarURL(img); setTest(img); }) .catch((err) => { console.log(err); }); } // Run the fetch one first time + return clean-up callback useEffect(() => { const abortController = new AbortController(); fillPageData(); return () => { console.log(avatarURL); URL.revokeObjectURL(avatarURL); abortController.abort(); }; }, []);
核心原因:闭包捕获的是state的快照
React组件的每次渲染都是基于状态快照的:当state更新时,组件会重新执行一遍,生成一个全新的函数作用域,这个作用域里的state变量是当前更新后的快照值。
你的useEffect依赖项是空数组[],意味着它只会在组件首次挂载时执行一次。此时创建的清理函数会牢牢抓住首次挂载作用域里的avatarURL值——也就是初始的userDummyPFP。后续setAvatarURL更新state时,组件虽然重新渲染,但首次挂载时生成的清理函数不会被更新,它依然持有旧作用域里的avatarURL,所以页面跳转触发清理时,打印的还是初始值。
而按钮能打印正确值,是因为按钮的点击事件是在组件重新渲染后的新作用域里创建的,捕获的是更新后的avatarURL快照。
解决方案:用useRef保存最新值
要让清理函数拿到最新的avatarURL,可以用useRef来存储这个值。ref的current属性是可变的,不会受组件渲染快照的限制,始终能拿到最新值:
import { useState, useEffect, useRef } from 'react'; const [avatarURL, setAvatarURL] = useState<string>(userDummyPFP); // 用ref同步保存最新的avatarURL const avatarURLRef = useRef<string>(userDummyPFP); // 每当avatarURL更新时,同步更新ref的current值 useEffect(() => { avatarURLRef.current = avatarURL; }, [avatarURL]); function fillPageData() { fetch("http://localhost:3000/get-user-pfp", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ userID: "UID-2", }), credentials: "include", }) .then((response) => response.blob()) .then((data) => { const img = URL.createObjectURL(data); setAvatarURL(img); }) .catch((err) => { console.log(err); }); } useEffect(() => { const abortController = new AbortController(); fillPageData(); return () => { // 这里用ref的current值,就能拿到最新的头像URL console.log(avatarURLRef.current); URL.revokeObjectURL(avatarURLRef.current); abortController.abort(); }; }, []);
内容的提问来源于stack exchange,提问作者John Smith

