You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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())
  }

原因分析
  1. 组件卸载导致状态丢失:父组件用条件渲染{selectview === 0 && <ListViewForm .../>}切换Tab,未选中的Tab组件会被React卸载,组件内部的表单状态(包括react-hook-form的状态)会被销毁,重新挂载时会重新初始化,回到初始状态。
  2. 不必要的表单重置: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,
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 11:52:06