为何第一段useEffect导致组件渲染失败?两段代码差异解析
问题描述
我在项目中使用第一段useEffect代码时,组件无法正确渲染:
useEffect(() => { setFavoriteContact((item) => { const favorites = window?.localStorage?.getItem("favorites") ? JSON.parse(`${localStorage.getItem("favorites")}`) : []; const favoritas = [...item, ...favorites]; contacts.forEach((item) => { const firstLetter = item.last_name.charAt(0).toUpperCase(); if (favoritas.includes(`${item.last_name} ${item.first_name}`)) { if (!favoriteContactGroup[firstLetter]) { favoriteContactGroup[firstLetter] = []; } favoriteContactGroup[firstLetter].push(item); } else { if (!normalContactGroup[firstLetter]) { normalContactGroup[firstLetter] = []; } normalContactGroup[firstLetter].push(item); } }); return favoritas; }); }, []);
调整为第二段代码后,组件可正常渲染,但存在数据展示延迟(无法立即将收藏联系人放到正确位置):
useEffect(() => { const favorites = window?.localStorage?.getItem("favorites") ? JSON.parse(`${localStorage.getItem("favorites")}`) : []; setFavoriteContact((item) => [...item, ...favorites]); }, []); contacts.forEach((item) => { const firstLetter = item.last_name.charAt(0).toUpperCase(); if (favoriteContact.includes(`${item.last_name} ${item.first_name}`)) { if (!favoriteContactGroup[firstLetter]) { favoriteContactGroup[firstLetter] = []; } favoriteContactGroup[firstLetter].push(item); } else { if (!normalContactGroup[firstLetter]) { normalContactGroup[firstLetter] = []; } normalContactGroup[firstLetter].push(item); } });
我的疑惑:为何第一段代码失效而第二段有效?控制台打印normalContactGroup和favoriteContactGroup的值是正确的,但组件未触发重渲染?
补充信息
favoriteContactGroup和normalContactGroup是普通空对象,初始定义:
const normalContactGroup: { [key: string]: Contact[] } = {}; const favoriteContactGroup: { [key: string]: Contact[] } = {};
- 后端返回的联系人数据结构示例:
const contactArray: Contact[] = [ { _id: "6561a98995095b8c3c8b38ca", contact_id: "6561a98995095b8c3c8b38c6", first_name: "Poppy", last_name: "Abba", default_number: "980891", phone_numbers: 2, createdAt: "2023-11-25T08:00:09.442Z", updatedAt: "2023-11-25T08:00:09.442Z", __v: 0, }, { _id: "6561a97695095b8c3c8b38c4", contact_id: "6561a97695095b8c3c8b38c0", first_name: "Berkley", last_name: "Arkham", default_number: "89701234", phone_numbers: 2, createdAt: "2023-11-25T07:59:50.618Z", updatedAt: "2023-11-25T07:59:50.618Z", __v: 0, }, ];
- 处理后分组对象结构示例:
{ "A": [ { "_id": "6561a98995095b8c3c8b38ca", "contact_id": "6561a98995095b8c3c8b38c6", "first_name": "Poppy", "last_name": "Abba", "default_number": "980891", "phone_numbers": 2, "createdAt": "2023-11-25T08:00:09.442Z", "updatedAt": "2023-11-25T08:00:09.442Z", "__v": 0 }, { "_id": "6561a97695095b8c3c8b38c4", "contact_id": "6561a97695095b8c3c8b38c0", "first_name": "Berkley", "last_name": "Arkham", "default_number": "89701234", "phone_numbers": 2, "createdAt": "2023-11-25T07:59:50.618Z", "updatedAt": "2023-11-25T07:59:50.618Z", "__v": 0 } ] }
问题原因与解决方案
第一段代码失效的核心原因
- 普通变量无法触发重渲染:
favoriteContactGroup和normalContactGroup是普通JS变量,不是React状态。React只有在状态/Props发生可检测的引用变化时才会触发重渲染,普通变量的修改不会被React感知,即便控制台打印值正确,UI也不会更新。 - 状态更新函数的闭包限制:
你在setFavoriteContact的更新函数内部修改分组变量,而这个更新函数是异步执行的闭包,此时修改的变量属于组件初始渲染的闭包上下文,后续组件无法捕获到这些变化。
第二段代码有效的原因
第二段把分组逻辑放到了组件渲染阶段(useEffect之外):第一次渲染时,分组逻辑用的是favoriteContact的初始值;当useEffect里的setFavoriteContact异步更新状态后,组件触发第二次重渲染,此时分组逻辑会基于更新后的favoriteContact重新计算,所以UI能显示正确结果,但因为状态更新是异步的,就出现了数据延迟的问题。
彻底解决问题的方案
方案1:用useMemo生成派生分组(推荐)
分组是基于contacts和favoriteContact计算出来的派生数据,用useMemo缓存计算结果,依赖项变化时自动重新计算,能立即拿到最新值:
const [favoriteContact, setFavoriteContact] = useState<string[]>([]); // 从localStorage加载收藏数据 useEffect(() => { const favorites = window?.localStorage?.getItem("favorites") ? JSON.parse(localStorage.getItem("favorites")!) : []; setFavoriteContact(favorites); }, []); // 用useMemo生成分组 const { favoriteContactGroup, normalContactGroup } = useMemo(() => { const favGroup: { [key: string]: Contact[] } = {}; const normalGroup: { [key: string]: Contact[] } = {}; contacts.forEach(item => { const firstLetter = item.last_name.charAt(0).toUpperCase(); const fullName = `${item.last_name} ${item.first_name}`; if (favoriteContact.includes(fullName)) { if (!favGroup[firstLetter]) favGroup[firstLetter] = []; favGroup[firstLetter].push(item); } else { if (!normalGroup[firstLetter]) normalGroup[firstLetter] = []; normalGroup[firstLetter].push(item); } }); return { favoriteContactGroup: favGroup, normalContactGroup: normalGroup }; }, [contacts, favoriteContact]);
方案2:把分组作为状态管理
如果需要对分组做更多可变操作,可以将其设为状态,在依赖项变化时更新:
const [favoriteContact, setFavoriteContact] = useState<string[]>([]); const [favoriteContactGroup, setFavoriteContactGroup] = useState<{ [key: string]: Contact[] }>({}); const [normalContactGroup, setNormalContactGroup] = useState<{ [key: string]: Contact[] }>({}); useEffect(() => { const favorites = window?.localStorage?.getItem("favorites") ? JSON.parse(localStorage.getItem("favorites")!) : []; setFavoriteContact(favorites); }, []); // 当依赖变化时更新分组状态 useEffect(() => { const favGroup: { [key: string]: Contact[] } = {}; const normalGroup: { [key: string]: Contact[] } = {}; contacts.forEach(item => { const firstLetter = item.last_name.charAt(0).toUpperCase(); const fullName = `${item.last_name} ${item.first_name}`; if (favoriteContact.includes(fullName)) { if (!favGroup[firstLetter]) favGroup[firstLetter] = []; favGroup[firstLetter].push(item); } else { if (!normalGroup[firstLetter]) normalGroup[firstLetter] = []; normalGroup[firstLetter].push(item); } }); setFavoriteContactGroup(favGroup); setNormalContactGroup(normalGroup); }, [contacts, favoriteContact]);
两种方案都能避免数据延迟,同时确保组件能正确感知分组变化并触发重渲染。
内容的提问来源于stack exchange,提问作者Zero Gear
相关产品推荐
相关产品推荐

