React useState首次渲染无法访问正确属性值 打印对象与属性值不一致
问题根因
- 异步逻辑处理错误:你在
mergeCookiesWithUserState函数内部调用了异步的getCookieValue读取Cookie,但没有等待异步操作完成就直接返回了状态对象,函数返回时Cookie还没读取完成,返回的状态还是初始默认值。 - 直接修改不可变对象:异步读取Cookie完成后,你直接修改了已经返回的状态对象属性,而没有通过React的
setState方法更新状态,React不会感知到这次修改,也不会触发组件重渲染,所以组件拿到的始终是初始值。 - 控制台输出的特性误导:Chrome控制台打印对象时保存的是对象的引用,你点击展开对象时才会读取对象的当前属性值,此时异步Cookie读取已经完成,你修改了原对象的属性,所以展开看到的是正确值;而打印具体属性(数字类型)时输出的是执行log那一刻的即时值,当时异步操作还没完成,所以拿到的是默认的0。
修复方案
- 不要在同步的状态初始化阶段处理异步逻辑,改为在
useEffect中读取Cookie,读取完成后调用setUserState更新状态触发重渲染。 - 禁止直接修改React状态对象,所有状态更新必须通过
setState完成。
修改后的自定义Hook参考代码
import { useState, useCallback, useEffect } from 'react' import { UserContextProps, UserStateProps, USER_STATE_DEFAULT } from '../context/UserContext' import { COOKIE_NAME_LANGUAGE, COOKIE_NAME_ADULT_FILTER, COOKIE_NAME_NEW_TAB, COOKIE_NAME_SEARCH_COUNT, getCookieValue, setCookie, } from '../helpers/_cookies' const cookiesName: { [key in keyof UserStateProps]?: any } = { numOfSearches: COOKIE_NAME_SEARCH_COUNT, language: COOKIE_NAME_LANGUAGE, adultContentFilter: COOKIE_NAME_ADULT_FILTER, openInNewTab: COOKIE_NAME_NEW_TAB, } export const useUserStateSyncedWithCookies = (): UserContextProps => { // 初始状态直接用默认值 const [userState, _setUserState] = useState<UserStateProps>(USER_STATE_DEFAULT) const setUserState = useCallback((nextState: Partial<UserStateProps>): void => { _setUserState((prevState) => { const newState = { ...prevState, ...nextState, } newState.adultContentFilter = Number(newState.adultContentFilter) newState.language = Number(newState.language) for (const key in nextState) { if (Object.prototype.hasOwnProperty.call(cookiesName, key)) { setCookie(cookiesName[key], newState[key], { expires: 365 }) } } return newState }) }, []) // 组件挂载后异步读取Cookie更新状态 useEffect(() => { const fetchAllCookies = async () => { const statePatch: Partial<UserStateProps> = {} // 并行读取所有需要同步的Cookie await Promise.all( Object.entries(cookiesName).map(async ([stateKey, cookieName]) => { const cookieValue = await getCookieValue(cookieName) if (cookieValue !== undefined) { statePatch[stateKey as keyof UserStateProps] = cookieValue } }) ) // 读取完成后统一更新状态,触发重渲染 if (Object.keys(statePatch).length > 0) { setUserState(statePatch) } } fetchAllCookies() }, [setUserState]) return { userState, setUserState, } }
额外说明
如果组件需要等待Cookie读取完成再渲染,可以加一个加载状态,避免组件拿到初始默认值执行错误逻辑。
内容的提问来源于stack exchange,提问作者Giorgia Sambrotta
相关产品推荐
相关产品推荐

