为何设置相同字符串状态时React仍触发重渲染?
问题解析
核心疑问
第一次点击按钮时,useCallback中调用setToken("unset"),父组件和子组件因按钮文本变化重渲染,符合预期。但第二次点击仍触发父组件重渲染,第三次点击却无任何反应——这才是我预期第二次点击该有的表现(每次点击都将token设为"unset")。
为何要手动添加if(token!=='unset'){setToken('unset');}判断,并把token加入回调依赖项,才能阻止后续重渲染?我原以为React只会在useState值更新且与旧值不同时触发重渲染。
控制台输出对比
| 无判断条件 | 有判断条件 |
|---|---|
| 渲染父组件,token: a7ib0PcX9 | 渲染父组件,token: a7ib0PcX9 |
| 渲染Logout | 渲染Logout |
| clicked | clicked |
| 渲染父组件,token: unset | 渲染父组件,token: unset |
| 渲染Logout | 渲染Logout |
| clicked | clicked |
| 渲染父组件,token: unset | clicked |
| clicked | clicked |
| clicked | |
| clicked |
代码示例
function Logout({ username, onLogout }) { console.log('render Logout'); return ( <> <button onClick={onLogout}>{username?('Logout '+username):'Buh Bye!'}</button> </> ); } const MemoizedLogout = React.memo(Logout); function AppLogout({ store }) { const [token, setToken] = useState('a7ib0PcX9'); console.log('render parent,','token: '+token); const onLogout=useCallback(() => { console.log('clicked'); store.username = undefined; setToken('unset'); }, [store]); return ( <div className="main"> <header> <MemoizedLogout username={store.username} onLogout={onLogout}/> </header> </div> ); } function App(){ return ( <> <AppLogout store={{username:'benz'}}/> </> ); }
原因说明
1. 第二次点击的额外重渲染
React中,调用setState时即使新值与旧值相同,也不保证完全跳过重渲染——这是一个优化策略,而非强制规则。具体到你的场景:
- 第一次点击后,
token变为unset,父组件重渲染,此时store.username被修改为undefined,导致子组件MemoizedLogout因username变化重渲染。 - 第二次点击时,你再次调用
setToken('unset'),虽然token值未变,但React仍会触发一次渲染流程(执行组件的渲染函数,所以你看到了“渲染父组件”的日志)。不过这次渲染后,React检测到状态未发生实际变化,会跳过DOM更新。 - 第三次点击时,React已经识别到该
setState调用不会改变状态,因此直接跳过渲染流程,只执行console.log('clicked')。
另外,你直接修改props对象store的属性(store.username = undefined)违反了React单向数据流规则,这可能导致组件props的隐性变化,进一步干扰渲染判断。
2. 为何需要手动判断
React的“相同值跳过重渲染”是优化而非强制行为,在开发模式(严格模式)、异步更新队列等场景下,都可能出现额外渲染。手动添加判断逻辑,能确保只有当状态确实需要更新时才调用setState,从根源上避免不必要的渲染触发,比依赖React的优化更可靠。
同时,正确的做法应该是将store.username改为组件内部状态,通过setState修改,而非直接修改props对象,这样能避免props隐性变化带来的渲染异常。
内容的提问来源于stack exchange,提问作者Ben Muircroft
相关产品推荐
相关产品推荐

