将组件移出return为何引发React额外重渲染?
折叠组件状态丢失问题:原因与解决方法
原始正常代码
最初直接渲染组件的写法运行完全正常:
<Container> <Carbs defaultCollapsed/> <Citrus defaultCollapsed/> <Gluten defaultCollapsed/> </Container>
Carbs等组件的内部结构:
<Carbs> <Collapsible defaultCollapsed={defaultCollapsed}> <List/> </Collapsible> </Carbs>
重构后的代码
为实现基于配置生成组件,重构后的代码如下:
const CarbsOption = ({defaultCollapsed}) => <Carbs defaultCollapsed={defaultCollapsed}/> const GlutenOption = ({defaultCollapsed}) => <Gluten defaultCollapsed={defaultCollapsed}/> const CitrusOption = ({defaultCollapsed}) => <Citrus defaultCollapsed={defaultCollapsed}/> const options = ['gluten', 'carbs', 'citrus'] const componentList = { gluten: { collapsed: true, component: GlutenOption, }, citrus: { collapsed: true, component: CitrusOption, }, carbs: { collapsed: true, component: CarbsOption, }, } return( <Container> { options.map((option,i) => { const Component = componentList[option].component; return <Component key={i} defaultCollapsed={componentList[option].collapsed}/> }) } </Container> )
问题现象
组件可正常渲染,但展开某组件(如Carbs)并修改List选项时,父组件触发重渲染,导致该组件自动恢复折叠状态。
原因分析
- 组件类型重复创建:如果重构代码写在父组件函数体内部,每次父组件重渲染时,
CarbsOption、GlutenOption这类包装组件都会被重新定义。React会判定这是全新的组件类型,进而卸载旧组件并重新挂载新组件——而新组件初始化时会使用defaultCollapsed的初始值true,之前展开的状态直接丢失。 - 非受控属性的局限性:
defaultCollapsed是非受控属性,仅在组件第一次挂载时生效。组件展开后,内部状态已经和该属性无关,但一旦组件被重新挂载,就会回到初始折叠状态。
解决方法
方法1:将包装组件移到父组件外部
把CarbsOption这类包装组件的定义移到父组件函数外部,避免每次重渲染都创建新的组件类型:
// 移到父组件函数外部,仅定义一次 const CarbsOption = ({defaultCollapsed}) => <Carbs defaultCollapsed={defaultCollapsed}/> const GlutenOption = ({defaultCollapsed}) => <Gluten defaultCollapsed={defaultCollapsed}/> const CitrusOption = ({defaultCollapsed}) => <Citrus defaultCollapsed={defaultCollapsed}/> const ParentComponent = () => { const options = ['gluten', 'carbs', 'citrus'] const componentList = { gluten: { collapsed: true, component: GlutenOption, }, citrus: { collapsed: true, component: CitrusOption, }, carbs: { collapsed: true, component: CarbsOption, }, } return( <Container> { options.map(option => { const Component = componentList[option].component; // 建议用option字符串作为key,比索引更稳定 return <Component key={option} defaultCollapsed={componentList[option].collapsed}/> }) } </Container> ) }
方法2:改用受控状态管理折叠状态
如果需要父组件控制或感知子组件的折叠状态,直接将折叠状态改为受控模式,由父组件统一管理:
const ParentComponent = () => { // 用state存储每个组件的折叠状态 const [collapsedStates, setCollapsedStates] = useState({ gluten: true, carbs: true, citrus: true }) // 处理折叠状态变化的回调 const handleCollapseChange = (option, isCollapsed) => { setCollapsedStates(prev => ({ ...prev, [option]: isCollapsed })) } const options = ['gluten', 'carbs', 'citrus'] const componentList = { gluten: { component: Gluten }, citrus: { component: Citrus }, carbs: { component: Carbs }, } return( <Container> { options.map(option => { const Component = componentList[option].component; return ( <Component key={option} // 替换defaultCollapsed为受控属性isCollapsed isCollapsed={collapsedStates[option]} onCollapseChange={(isCollapsed) => handleCollapseChange(option, isCollapsed)} /> ) }) } </Container> ) }
注意:需要同步修改Carbs等子组件,让内部的Collapsible使用受控的isCollapsed属性,并传递状态变化回调给父组件。
方法3:简化代码,去掉冗余包装组件
CarbsOption这类包装组件完全是冗余的,可以直接在componentList中引用原组件:
const ParentComponent = () => { const options = ['gluten', 'carbs', 'citrus'] const componentList = { gluten: { collapsed: true, component: Gluten }, citrus: { collapsed: true, component: Citrus }, carbs: { collapsed: true, component: Carbs }, } return( <Container> { options.map(option => { const Component = componentList[option].component; return <Component key={option} defaultCollapsed={componentList[option].collapsed}/> }) } </Container> ) }
既减少了冗余代码,也避免了中间组件重复创建的问题。
内容的提问来源于stack exchange,提问作者Alyona
相关产品推荐
相关产品推荐

