React组件切换后数据丢失:已用React.memo仍无效
我有两个React组件:BucketTabs与BucketForms。切换标签页时,下方展示对应的表单。但每次从一个标签页切换到另一个,再切回原标签页时,BucketForms中的内容会丢失——这些数据本该存在组件的state里。我已经给BucketForms加了React.memo,但数据还是会丢,请问问题根源是什么?该怎么解决?
我的代码
BucketTabs组件
import { BucketForms } from '~components/BucketForms/BuckForms' export const BucketTabs: React.FC = () => { const items = useMemo<ContentTabsItem[]>((): ContentTabsItem[] => { return [ { title: '1', renderContent: () => <BucketForms key="1" bucketCategory="1" />, }, { title: '2', renderContent: () => <BucketForms key="2" bucketCategory="2" />, }, ] }, []) return ( <div className="row"> <div className="col"> <ContentTabs items={tabs} kind="tabs" /> </div> </div> ) }
BucketForms组件
function PropsAreEqual(prev, next) { const result = prev.bucketCategory === next.bucketCategory; return result; } interface IData { portfolioValue?: number } export const BucketForms: React.FC<IProps> = React.memo(props => { const { bucketCategory } = props const [data, setData] = useState<IData>({ }) const view = ({ portfolioValue, }: IData) => { return ( <> <div className="row portfolio"> <FormNumericInput key="input-portfolio-value" name="portfolioValue" required value={portfolioValue} /> </div> </> ) } return ( <Form onChange={e => { setData({ ...data, ...e, }) }} > {view(data)} </Form> ) }, PropsAreEqual)
问题根源
React.memo的作用是阻止组件在props未变化时的不必要重渲染,但它解决不了组件卸载导致的state丢失问题。你的场景里,ContentTabs组件在切换标签时,会把非激活标签对应的BucketForms组件从DOM中移除——这会触发组件卸载,组件内部的state会被完全销毁。当你切回原标签时,会重新创建一个全新的BucketForms实例,state自然回到初始值,之前输入的数据也就没了。
解决方案
1. 将状态提升到父组件(推荐)
把BucketForms的state移到父组件BucketTabs中统一管理,通过props把数据和更新方法传递给子组件。这样即使子组件被卸载,数据也会保存在父组件的state里,切回标签时能直接恢复。
修改后的BucketTabs:
import { BucketForms } from '~components/BucketForms/BuckForms' // 引入IData类型(需要和BucketForms中的定义一致) interface IData { portfolioValue?: number } export const BucketTabs: React.FC = () => { // 用对象存储不同标签的表单数据,key对应bucketCategory const [tabData, setTabData] = useState<Record<string, IData>>({ '1': {}, '2': {} }) // 统一处理不同标签的数据更新 const updateTabData = (bucketCategory: string, newData: IData) => { setTabData(prev => ({ ...prev, [bucketCategory]: newData })) } const items = useMemo<ContentTabsItem[]>((): ContentTabsItem[] => { return [ { title: '1', renderContent: () => ( <BucketForms key="1" bucketCategory="1" data={tabData['1']} onChange={(newData) => updateTabData('1', newData)} /> ), }, { title: '2', renderContent: () => ( <BucketForms key="2" bucketCategory="2" data={tabData['2']} onChange={(newData) => updateTabData('2', newData)} /> ), }, ] }, [tabData]) return ( <div className="row"> <div className="col"> <ContentTabs items={items} kind="tabs" /> </div> </div> ) }
修改后的BucketForms:
function PropsAreEqual(prev, next) { // 对比bucketCategory和data,确保props不变时不重渲染 return prev.bucketCategory === next.bucketCategory && JSON.stringify(prev.data) === JSON.stringify(next.data); } interface IData { portfolioValue?: number } interface IProps { bucketCategory: string; data: IData; onChange: (data: IData) => void; } export const BucketForms: React.FC<IProps> = React.memo((props) => { const { bucketCategory, data, onChange } = props const view = ({ portfolioValue }: IData) => { return ( <> <div className="row portfolio"> <FormNumericInput key="input-portfolio-value" name="portfolioValue" required value={portfolioValue} /> </div> </> ) } return ( <Form onChange={e => { onChange({ ...data, ...e }) }} > {view(data)} </Form> ) }, PropsAreEqual)
2. 让ContentTabs保留所有标签的组件实例
如果你的ContentTabs组件支持,可以配置为不卸载非激活标签的内容,而是通过CSS隐藏(比如display: none)。这样所有BucketForms组件都会一直存在于DOM中,state不会被销毁。
如果是自定义的ContentTabs组件,修改渲染逻辑:不要用条件渲染(比如{activeTab === '1' && <BucketForms />}),而是渲染所有标签内容,通过类名控制显示和隐藏:
// 示例:自定义ContentTabs的渲染逻辑 return ( <div> {/* 标签头 */} <div className="tabs-header"> {items.map(item => ( <button key={item.title} className={activeKey === item.key ? 'active' : ''} onClick={() => setActiveKey(item.key)} > {item.title} </button> ))} </div> {/* 标签内容:全部渲染,用CSS控制显示 */} <div className="tabs-content"> {items.map(item => ( <div key={item.key} className={activeKey === item.key ? 'tab-content active' : 'tab-content hidden'} > {item.renderContent()} </div> ))} </div> </div> )
对应的CSS:
.tab-content.hidden { display: none; } .tab-content.active { display: block; }
3. 用本地存储持久化数据
如果不需要把数据存在父组件,也可以把表单数据存在localStorage或sessionStorage中,组件初始化时从本地存储读取恢复数据。这种方式即使刷新页面,数据也不会丢失。
修改后的BucketForms:
function PropsAreEqual(prev, next) { const result = prev.bucketCategory === next.bucketCategory; return result; } interface IData { portfolioValue?: number } export const BucketForms: React.FC<IProps> = React.memo((props) => { const { bucketCategory } = props // 初始化时从localStorage读取对应标签的数据 const [data, setData] = useState<IData>(() => { const savedData = localStorage.getItem(`bucket-form-${bucketCategory}`); return savedData ? JSON.parse(savedData) : {}; }) // 数据变化时同步保存到localStorage useEffect(() => { localStorage.setItem(`bucket-form-${bucketCategory}`, JSON.stringify(data)); }, [data, bucketCategory]) const view = ({ portfolioValue }: IData) => { return ( <> <div className="row portfolio"> <FormNumericInput key="input-portfolio-value" name="portfolioValue" required value={portfolioValue} /> </div> </> ) } return ( <Form onChange={e => { setData({ ...data, ...e }) }} > {view(data)} </Form> ) }, PropsAreEqual)
内容的提问来源于stack exchange,提问作者Ae Leung

