React多Tab表单:如何避免不必要重渲染与状态丢失?
问题描述
实现了一个包含三个Tab的组件,每个Tab对应一个表单。在第一个表单输入内容后切换到第二个Tab,再切回第一个时,表单会回到初始状态,需要解决这种数据丢失的问题。
父组件代码
"use client"; import { MODELS_CONSTANTS } from "@/shared/constants/models.constants"; import CommonService from "@/shared/services/common/common.service"; import FuseSvgIcon from "@fuse/core/FuseSvgIcon"; import { Box, Tab, Tabs, Typography } from "@mui/material"; import { useEffect, useState } from "react"; import ListViewAction from "./ListViewAction"; import ListViewFields from "./ListViewFields"; import ListViewForm from "./ListViewForm"; import ListViewProvider from "./context/listViewContext"; const commonService = new CommonService(); const Listview = (props) => { const [selectview, setselectview] = useState(0); const [fieldData, setFieldData] = useState([]); const [actionData, setActionData] = useState([]); const listview = ["List View Form", "List View Field", "List View Action"]; const handleclicktab = (event, index) => { setselectview(index); }; const getFieldsData = async () => { const response = await commonService.getDataByField(MODELS_CONSTANTS?.LIST_FIELDS, "list_view_id", props?.rowData._id); if (response.status === 200) { setFieldData(response.data.data); } }; const getActionData = async () => { const response = await commonService.getDataByField(MODELS_CONSTANTS?.LIST_ACTIONS, "list_view_id", props?.rowData._id); if (response.status === 200) { setActionData(response.data.data); } }; useEffect(() => { if (props?.rowData._id) { getFieldsData(); getActionData(); } }, []); return ( <ListViewProvider> <Box sx={{ borderBottom: 1, borderColor: "divider" }}> <Box className="flex justify-between items-center p-24"> <Typography variant="h5">List View Setup</Typography> <FuseSvgIcon className="cursor-pointer" onClick={() => props.onClose()}> heroicons-outline:x-mark </FuseSvgIcon> </Box> <Tabs value={selectview} variant="fullWidth" onChange={handleclicktab}> {listview.map((item, index) => { return <Tab key={index} label={item} disabled={!Object.keys(props?.rowData).length && index !== 0} />; })} </Tabs> </Box> <Box sx={{ padding: 2 }}> {selectview === 0 && <ListViewForm formRowData={props.rowData} />} {selectview === 1 && <ListViewFields fieldData={fieldData} formRowDataId={props.rowData._id} />} {selectview === 2 && <ListViewAction actionData={actionData} formId={props.rowData._id} />} </Box> </ListViewProvider> ); }; export default Listview;
第一个Tab的表单组件代码
const ListViewForm = ({ formRowData }) => { const { setListModule, listViewFormData, setListViewFormData } = useContext(ListViewContext); const dispatch = useAppDispatch(); const [open, setOpen] = useState(false) type FormType = { list_name?: { name: string; value: string } | string; display_type?: { display_type: string; value: string } | string; list_type?: { list_type: string; value: string } | string; list_description?: { description: string; value: string } | string; api_registry?: { api_registry: string; value: string } | string; module?: { module: string; value: string } | string; reference_type?: { name: string; value: string } | string; reference_id?: { reference_id: string; value: string } | string; drawer_width?: { drawer_width: string; value: string } | string; size?: { class: string; value: string } | string; tile_type?: { tile_type: string; value: string } | string; selected_tab?: { selected_tab: string; value: string } | string; unique_fields?: any; enable_delete?: boolean; enable_inline_edit?: boolean; common_title?: { common_title: string; value: string } | string; }; const display_type = [ { name: "--", value: "--" }, { name: "Tile", value: "tile" }, { name: "Grid", value: "grid" }, ]; const list_type = [ { name: "--", value: "--" }, { name: "Search", value: "search" }, { name: "Processing", value: "processing" }, { name: "Report", value: "report" }, ]; const drawer_width = [ { name: "--", value: "--" }, { name: "100%", value: "100%" }, { name: "75%", value: "75%" }, { name: "50%", value: "50%" }, { name: "25%", value: "25%" }, ]; const selected_tab = [{ name: "--", value: "--" }]; const unique_fields = [ { name: "--", value: "--" }, { name: "Order ID", value: "orderid" }, { name: "InfoCard", value: "infocard" }, ]; const { control, handleSubmit, watch, setValue, reset } = useForm<FormType>({ mode: "onChange", defaultValues: listViewFormData, }); const formData = watch(); const formValues = useWatch({ control }); // Watch form values useEffect(() => { setListViewFormData(formValues); // Update state on value change }, [formValues, setListViewFormData]); const [modules, setModules] = useState([]); const [initAction, setInitAction] = useState([]); const [reference, setreference] = useState([]); const [referenceid, setreferenceid] = useState([]); const [classtype, setclasstype] = useState([]); const [tiletype, settiletype] = useState([]); useEffect(() => { commonService.getAllData(MODELS_CONSTANTS.MODULES).then((res) => { const module_arr = res?.data?.data.map((item: any) => ({ name: item?.entity_name, value: item?.entity_code, _id: item?.collection_id, })); setModules(module_arr); }); commonService.getAllData(MODELS_CONSTANTS.API_REGISTRIES).then((res) => { const module_arr = res?.data?.data.map((item: any) => ({ name: item?.api_name, value: item?._id, })); setInitAction(module_arr); }); commonService.getDataByField(MODELS_CONSTANTS.VALUE_SET_DETAILS, "vs_code", "FORM_COMPONENTS").then((res) => { const module_arr = res?.data?.data.map((item: any) => ({ name: item?.vsd_code, value: item?.vsd_code, })); setreference(module_arr); }); commonService.getAllData(MODELS_CONSTANTS.FORM_SETUP).then((res) => { const module_arr = res?.data?.data.map((item: any) => ({ name: item?.form_name, value: item?._id, })); setreferenceid(module_arr); }); commonService.getDataByField(MODELS_CONSTANTS.VALUE_SET_DETAILS, "vs_code", "CLASS_TYPE").then((res) => { const module_arr = res?.data?.data.map((item: any) => ({ name: item?.vsd_code, value: item?.vsd_code, })); setclasstype(module_arr); }); commonService.getDataByField(MODELS_CONSTANTS.VALUE_SET_DETAILS, "vs_code", "TILE_TYPES").then((res) => { const module_arr = res?.data?.data.map((item: any) => ({ name: item?.vsd_code, value: item?._id, })); settiletype(module_arr); }); }, []); useEffect(() => { reset({ list_name: formRowData?.list_view_name, display_type: display_type.find((a) => a.value === formRowData.display_type), list_type: list_type.find((a) => a.value === formRowData.list_type), list_description: formRowData?.list_view_description, api_registry: initAction.find((a) => a.value === formRowData?.api_id) || "", module: modules.find((a) => a.value === formRowData?.module_id), unique_fields: formRowData.unique_fields, drawer_width: drawer_width.find((a) => a.name === formRowData.drawer_width), reference_type: reference.find((a) => a.name === formRowData.reference_type), reference_id: referenceid.find((a) => a.value === formRowData.reference_id), tile_type: tiletype.find((a) => a.value === formRowData.tile_type) || "", size: classtype.find((a) => a.value === formRowData.class_type), enable_delete: formRowData.show_delete_yn, enable_inline_edit: formRowData.inline_edit_yn, common_title: formRowData?.common_title, }); }, [tiletype]); const return_id = (a, key) => { if (typeof a[key] === "object") { return a[key].value; } else { return a[key]; } }; const array_id = (a, key) => { if (a[key] === "array") { return a[key].value; } else { return a[key]; } }; const onSubmit = async (data: FormType) => { const listformpayload = { ...formRowData, list_view_name: data.list_name ? return_id(data, "list_name") : "", display_type: data.display_type ? return_id(data, "display_type") : "", list_type: data.list_type ? return_id(data, "list_type") : "", list_view_description: data.list_description ? return_id(data, "list_description") : "", api_id: data.api_registry ? return_id(data, "api_registry") : "", module_id: data.module ? return_id(data, "module") : "", reference_type: data.reference_type ? return_id(data, "reference_type") : "", reference_id: data.reference_id ? return_id(data, "reference_id") : "", drawer_width: data.drawer_width ? return_id(data, "drawer_width") : "", class_type: data.size ? return_id(data, "size") : "", tile_type: data.tile_type ? return_id(data, "tile_type") : "", selected_tab: data.selected_tab ? return_id(data, "selected_tab") : "", unique_fields: data.unique_fields ? array_id(data, "unique_fields") : "", common_title: data.common_title, show_delete_yn: data.enable_delete, inline_edit_yn: data.enable_inline_edit, }; const response = await commonService.saveRecord(MODELS_CONSTANTS.LIST_VIEW, listformpayload); if (response.status === 200) { dispatch(showMessage({ message: "Data Saved Successfully" })); } }; const setModelSchema = (item) => { setListModule(item); dispatch(closeDialog()) }
原因分析
- 组件卸载导致状态丢失:父组件用条件渲染
{selectview === 0 && <ListViewForm .../>}切换Tab,未选中的Tab组件会被React卸载,组件内部的表单状态(包括react-hook-form的状态)会被销毁,重新挂载时会重新初始化,回到初始状态。 - 不必要的表单重置:
ListViewForm中存在依赖tiletype的useEffect,每次tiletype更新都会调用reset重置表单,覆盖用户输入的内容。
解决方案
方案1:提升表单状态到Context(推荐)
把表单状态存储到全局Context中,组件卸载时状态不会丢失,重新挂载时直接复用。
步骤1:修改Context定义
// context/listViewContext.js import { createContext, useState } from 'react'; const ListViewContext = createContext(); export const ListViewProvider = ({ children }) => { const [listViewFormData, setListViewFormData] = useState({}); // 添加持久化表单值的状态 const [persistedFormValues, setPersistedFormValues] = useState({}); return ( <ListViewContext.Provider value={{ listViewFormData, setListViewFormData, persistedFormValues, setPersistedFormValues }}> {children} </ListViewContext.Provider> ); }; export default ListViewContext;
步骤2:修改ListViewForm组件
const ListViewForm = ({ formRowData }) => { const { setListModule, persistedFormValues, setPersistedFormValues } = useContext(ListViewContext); const dispatch = useAppDispatch(); const [open, setOpen] = useState(false) // ... 保留原类型定义和选项数组 // 初始化值优先用持久化的表单值,没有则用formRowData const initialValues = persistedFormValues || { list_name: formRowData?.list_view_name, display_type: display_type.find((a) => a.value === formRowData.display_type), list_type: list_type.find((a) => a.value === formRowData.list_type), list_description: formRowData?.list_view_description, api_registry: initAction.find((a) => a.value === formRowData?.api_id) || "", module: modules.find((a) => a.value === formRowData?.module_id), unique_fields: formRowData.unique_fields, drawer_width: drawer_width.find((a) => a.name === formRowData.drawer_width), reference_type: reference.find((a) => a.name === formRowData.reference_type), reference_id: referenceid.find((a) => a.value === formRowData.reference_id), tile_type: tiletype.find((a) => a.value === formRowData.tile_type) || "", size: classtype.find((a) => a.value === formRowData.class_type), enable_delete: formRowData.show_delete_yn, enable_inline_edit: formRowData.inline_edit_yn, common_title: formRowData?.common_title, }; const { control, handleSubmit, watch, setValue, reset } = useForm<FormType>({ mode: "onChange", defaultValues: initialValues, }); const formValues = useWatch({ control }); // 实时同步表单值到Context useEffect(() => { setPersistedFormValues(formValues); }, [formValues, setPersistedFormValues]); // ... 保留原获取数据的useEffect // 仅在无持久化值时初始化表单 useEffect(() => { if (!Object.keys(persistedFormValues).length) { reset(initialValues); } }, [persistedFormValues, reset, initialValues]); // ... 保留原提交和其他函数 };
方案2:保持组件挂载(不卸载)
用CSS隐藏未选中的Tab内容,而非卸载组件,状态会被保留。
修改父组件的渲染部分:
<Box sx={{ padding: 2 }}> {listview.map((_, index) => ( <div key={index} style={{ display: selectview === index ? 'block' : 'none' }}> {index === 0 && <ListViewForm formRowData={props.rowData} />} {index === 1 && <ListViewFields fieldData={fieldData} formRowDataId={props.rowData._id} />} {index === 2 && <ListViewAction actionData={actionData} formId={props.rowData._id} />} </div> ))} </Box>
注意:此方式会增加初始渲染开销,适合表单逻辑不复杂的场景。
方案3:优化表单重置逻辑
移除不必要的reset调用,避免覆盖用户输入:
useEffect(() => { // 仅在组件首次挂载且无用户输入时重置 if (!Object.keys(persistedFormValues).length) { reset({ list_name: formRowData?.list_view_name,
相关产品推荐
相关产品推荐

