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
相关产品推荐
相关产品推荐

