React tab组件状态未变更时第二次点击触发父组件重渲染问题
原因说明
这个是React的正常内置行为,不属于bug,核心逻辑来自React状态更新的 bailout(指状态无变化时跳过更新的机制)设计:
按照React官方给出的规则,当你调用setState传入的新值和当前状态严格相等(通过Object.is比较)时,React会跳过子组件渲染、DOM更新、副作用执行等深层操作,但允许对当前组件本身执行一次额外的重渲染再做 bailout,这个额外渲染不会深入子树,性能损耗可以忽略。
你遇到的现象刚好符合这个规则:
- 初始状态是
a,第一次点击First Title调用setSelectTab("a"),React直接判定状态无变化,连父组件额外渲染都跳过,所以无日志打印。 - 首次点击
Second Title,状态从a变为b,触发正常父组件重渲染,打印parent render符合预期。 - 第二次点击
Second Title,调用setSelectTab("b"),新值和当前状态相等,React触发一次父组件的额外渲染(所以打印了parent render),之后判定状态无变化,跳过子组件渲染、DOM更新等后续操作,所以看不到child render打印,页面也没有变化。 - 第三次及之后点击
Second Title,React已经确认该状态无变更需要处理,就不再触发额外渲染,所以无日志打印。
可选优化方案
如果你想彻底避免这一次额外的父组件渲染,可以在触发状态更新前先做值判断,相等时就不调用setSelectTab:
父组件用useRef存当前选中的tab值,封装稳定的更新函数即可:
import { useState, useCallback, useRef, useEffect } from "react"; import "./styles.scss"; import MemoizedTab from "./tab"; export default function App() { const [selectTab, setSelectTab] = useState("a"); // 用ref存最新的选中值,避免回调依赖变化 const selectTabRef = useRef(selectTab); useEffect(() => { selectTabRef.current = selectTab; }, [selectTab]); // 封装稳定的更新函数,值相等时不触发setState const safeSetSelectTab = useCallback((newTab) => { if (newTab !== selectTabRef.current) { setSelectTab(newTab); } }, []); console.log("parent render"); return ( <div className="App"> <div className="tab-list"> <MemoizedTab tab={"a"} title={"First Title"} setSelectTab={safeSetSelectTab} /> <MemoizedTab tab={"b"} title={"Second Title"} setSelectTab={safeSetSelectTab} /> <MemoizedTab tab={"c"} title={"Third Title"} setSelectTab={safeSetSelectTab} /> </div> {selectTab === "a" && <div>this is a</div>} {selectTab === "b" && <div>this is b</div>} {selectTab === "c" && <div>this is c</div>} </div> ); }
内容的提问来源于stack exchange,提问作者dev_el
相关产品推荐
相关产品推荐

