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

React Hook Form嵌套对话框表单提交触发父表单的问题求助

解决React Hook Form对话框提交触发父表单提交的问题

问题根源

你的问题出在对话框组件共享了父表单的useFormContext上下文,导致对话框的提交逻辑直接触发了父表单的handleSubmit;另外如果未给对话框设置独立表单元素,MUI按钮默认的submit类型也会通过事件冒泡触发父表单提交,这时候preventDefault()和stopPropagation()无法阻断React Hook Form内部的表单提交逻辑。

可行解决方案

核心思路是让对话框拥有独立的表单实例,校验通过后再通过回调更新父表单的数组数据,同时复用抽离的Zod Schema保证校验规则一致。

1. 抽离独立的数组项Schema

把数组项的校验规则单独抽离,父表单和对话框表单共享这个Schema:

import { z } from 'zod';

// 独立的数组项校验规则
const ItemSchema = z.object({
  name: z.string().min(1, '名称不能为空'),
  value: z.number().min(0, '数值不能为负数'),
});

// 父表单整体Schema
const FormSchema = z.object({
  items: z.array(ItemSchema),
});

2. 父组件管理数组与对话框状态

父组件使用useFieldArray维护数组字段,传递回调给对话框用于更新数据:

import { useForm, useFieldArray } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Button, Box, TextField } from '@mui/material';
import ItemDialog from './ItemDialog';
import { FormSchema, ItemSchema } from './schemas';

type FormValues = z.infer<typeof FormSchema>;
type ItemValues = z.infer<typeof ItemSchema>;

export default function ParentForm() {
  const { control, handleSubmit, formState: { errors } } = useForm<FormValues>({
    resolver: zodResolver(FormSchema),
    defaultValues: { items: [] },
  });

  const { fields, append, update, remove } = useFieldArray({
    control,
    name: 'items',
  });

  const [dialogOpen, setDialogOpen] = React.useState(false);
  const [editingItem, setEditingItem] = React.useState<{ index: number; data: ItemValues } | null>(null);

  // 父表单提交逻辑
  const onFormSubmit = (data: FormValues) => {
    console.log('父表单提交数据:', data);
    // 这里处理父表单的提交操作
  };

  // 打开添加/编辑对话框
  const openDialog = (item?: { index: number; data: ItemValues }) => {
    setEditingItem(item || null);
    setDialogOpen(true);
  };

  // 对话框保存回调:更新父表单数组
  const handleItemSave = (itemData: ItemValues) => {
    if (editingItem) {
      // 编辑模式:更新对应索引的数组项
      update(editingItem.index, itemData);
    } else {
      // 添加模式:追加新数组项
      append(itemData);
    }
    setDialogOpen(false);
  };

  return (
    <Box component="form" onSubmit={handleSubmit(onFormSubmit)} sx={{ p: 3 }}>
      {/* 展示已有数组项 */}
      {fields.map((item, index) => (
        <Box key={item.id} sx={{ display: 'flex', gap: 2, mb: 2, alignItems: 'center' }}>
          <TextField
            disabled
            value={item.name}
            sx={{ flex: 1 }}
            label="名称"
          />
          <TextField
            disabled
            type="number"
            value={item.value}
            sx={{ flex: 1 }}
            label="数值"
          />
          <Button onClick={() => openDialog({ index, data: item })}>编辑</Button>
          <Button onClick={() => remove(index)}>删除</Button>
        </Box>
      ))}

      {/* 添加按钮与父表单提交按钮 */}
      <Button onClick={() => openDialog()} sx={{ mr: 2 }}>添加新项</Button>
      <Button type="submit" variant="contained">提交父表单</Button>

      {/* 独立的对话框组件 */}
      <ItemDialog
        open={dialogOpen}
        onClose={() => setDialogOpen(false)}
        initialData={editingItem?.data}
        onSave={handleItemSave}
      />
    </Box>
  );
}

3. 对话框组件创建独立表单实例

对话框不使用父表单的上下文,自行初始化useForm完成校验,通过回调传递数据给父组件:

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Dialog, DialogContent, DialogActions, Button, TextField, Box } from '@mui/material';
import { ItemSchema } from './schemas';

type ItemValues = z.infer<typeof ItemSchema>;

type ItemDialogProps = {
  open: boolean;
  onClose: () => void;
  initialData?: ItemValues;
  onSave: (data: ItemValues) => void;
};

export default function ItemDialog({ open, onClose, initialData, onSave }: ItemDialogProps) {
  const { register, handleSubmit, formState: { errors } } = useForm<ItemValues>({
    resolver: zodResolver(ItemSchema),
    defaultValues: initialData || { name: '', value: 0 },
  });

  // 对话框自身的提交逻辑
  const onDialogSubmit = (data: ItemValues) => {
    onSave(data);
  };

  return (
    <Dialog open={open} onClose={onClose}>
      <DialogContent sx={{ minWidth: 400 }}>
        {/* 对话框独立表单,与父表单完全隔离 */}
        <Box component="form" onSubmit={handleSubmit(onDialogSubmit)}>
          <TextField
            label="名称"
            {...register('name')}
            error={!!errors.name}
            helperText={errors.name?.message}
            fullWidth
            margin="normal"
          />
          <TextField
            label="数值"
            type="number"
            {...register('value', { valueAsNumber: true })}
            error={!!errors.value}
            helperText={errors.value?.message}
            fullWidth
            margin="normal"
          />
          <DialogActions>
            <Button onClick={onClose}>取消</Button>
            {/* 触发对话框自身的表单提交 */}
            <Button type="submit" variant="contained">保存</Button>
          </DialogActions>
        </Box>
      </DialogContent>
    </Dialog>
  );
}

为什么之前的方法无效?

当对话框共享父表单的useFormContext时,调用handleSubmit本质是触发父表单的提交逻辑,此时即使阻止DOM事件的默认行为,React Hook Form内部的表单提交流程已经被触发。通过让对话框拥有独立表单实例,彻底隔离了父子表单的提交逻辑,从根源解决问题。

内容的提问来源于stack exchange,提问作者baitendbidz

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 12:22:02