TypeScript React Hook Form选择框组件提交失败求助
问题描述
在TypeScript React中开发了三个联动下拉框组件,使用React Hook Form提交表单时,三个选择框的字段值为空,无法提交对应数据,但表单其余部分正常。相关代码如下:
TypeScript组件代码
type CatProdPickerProps = { name: string, control: any } export const CategoryProductPicker = ({name, control}: CatProdPickerProps) => { const [selectedCat, setSelectedCat] = useState(''); const [selectedProd, setSelectedProd] = useState([]); const [selectedColor, setSelectedColor] = useState<any[]>([]); const { data, isError, isLoading } = useQuery({ queryKey: ['orders'], queryFn: () => fetch('http://localhost:8080/portal').then((res) => { if (!res.ok) { throw new Error(`Network response was not ok, status ${res.status}`); } return res.json(); }) }) if (isLoading) { return <div>Loading...</div>; } if (isError) { return ( <div> <h1>Error Occured</h1> <p>Please try again later</p> </div> ) } const handleCategoryChange = (categoryName: string) => { setSelectedCat(categoryName); // Find the selected category in data and get its product names const selectedCategoryData = data?.find( (item: any) => item.categoryName === categoryName); if (selectedCategoryData) { // Extract product names as an array of strings const productNames = selectedCategoryData.productName.map( (p: { productName: string }) => p.productName ); setSelectedProd(productNames); } else { setSelectedProd([]); // Reset if no matching category } if (selectedCategoryData) { // Extract color names as an array of strings const colorNames = selectedCategoryData.colorName.map( (p: { colorName: string }) => p.colorName ); setSelectedColor(colorNames); } else { setSelectedColor([]); // Reset if no matching color } }; return ( <div> <Controller name={name} control={control} render={({ field: { onChange, value } }) => ( <><select className="select" name="categoryName" onChange={(e) => handleCategoryChange(e.target.value)}> <option value={0}>Select Category</option> {data?.map((cat: any) => ( <option key={cat.ordId} value={cat.categoryName}>{cat.categoryName}</option> ))} </select> <select className="select" name="productName" > <option value={0}>Select Product</option> {selectedProd.map((product, index) => ( <option key={index} value={product}> {product} </option> ))} </select> <select className="select" name="colorName" > <option value={0}>Select Colors</option> {selectedColor.map((color, index) => ( <option key={index} value={color}> {color} </option> ))} </select></> )} /> </div> ); }
React Hook Form表单代码
export const CategoryAdd = () => { const { register, handleSubmit, formState: { isSubmitting }, reset, control, } = useForm({ defaultValues: { } }); const onSubmit = async (data: FieldValues) => { await fetch("http://localhost:8080/portal/createOrder", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data) }) }; return ( <Container maxWidth="lg"> <Box marginTop={10} sx={{ height: '20vh' }}> <form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-y-2"> <CategoryProductPicker control={control} name="" /> <Stack spacing={10} direction="row"> <Button variant="contained" disabled={isSubmitting} type="submit" className="bg-blue-500 disabled:bg-gray-500 py-2 rounded" > Submit </Button> </Stack> </form> </Box> </Container > ); }
问题排查与解决方案
核心问题分析
Controller的name属性为空:调用CategoryProductPicker时传入的name是空字符串,React Hook Form无法识别该字段,无法收集数据。- 未同步表单状态:下拉框变化仅更新了组件内部
useState,未调用React Hook Form的onChange方法,导致表单无法捕获值变化。 - 单个
Controller绑定多字段:用一个Controller包裹三个下拉框不符合React Hook Form逻辑,每个表单字段需独立绑定。
具体修改步骤
1. 传入有效的字段名前缀
修改CategoryAdd中的组件调用,传入有意义的name,用于后续嵌套字段命名:
<CategoryProductPicker control={control} name="order" />
2. 重构CategoryProductPicker,使用多Controller管理字段
每个下拉框对应独立的Controller,联动更新选项同时同步表单状态:
import { useController, Control } from 'react-hook-form'; import { useState, useEffect } from 'react'; import { useQuery } from '@tanstack/react-query'; type OrderFormValues = { categoryName: string; productName: string; colorName: string; }; type CatProdPickerProps = { name: string; control: Control<{ [key: string]: OrderFormValues }>; }; export const CategoryProductPicker = ({ name, control }: CatProdPickerProps) => { const [productOptions, setProductOptions] = useState<string[]>([]); const [colorOptions, setColorOptions] = useState<string[]>([]); const { data, isError, isLoading } = useQuery({ queryKey: ['orders'], queryFn: () => fetch('http://localhost:8080/portal').then((res) => { if (!res.ok) { throw new Error(`Network response was not ok, status ${res.status}`); } return res.json(); }), }); if (isLoading) return <div>Loading...</div>; if (isError) return ( <div> <h1>Error Occured</h1> <p>Please try again later</p> </div> ); // 监听分类字段变化,联动更新产品和颜色选项 const { field: categoryField } = useController({ control, name: `${name}.categoryName`, defaultValue: '', }); useEffect(() => { if (!categoryField.value) { setProductOptions([]); setColorOptions([]); return; } const selectedCategoryData = data?.find( (item: any) => item.categoryName === categoryField.value ); if (selectedCategoryData) { const productNames = selectedCategoryData.productName.map( (p: { productName: string }) => p.productName ); const colorNames = selectedCategoryData.colorName.map( (p: { colorName: string }) => p.colorName ); setProductOptions(productNames); setColorOptions(colorNames); } else { setProductOptions([]); setColorOptions([]); } }, [categoryField.value, data]); return ( <div className="flex flex-col gap-y-2"> {/* 分类选择框 */} <select className="select" value={categoryField.value} onChange={(e) => categoryField.onChange(e.target.value)} > <option value="">Select Category</option> {data?.map((cat: any) => ( <option key={cat.ordId} value={cat.categoryName}> {cat.categoryName} </option> ))} </select> {/* 产品选择框 */} <Controller control={control} name={`${name}.productName`} defaultValue="" render={({ field: { onChange, value } }) => ( <select className="select" value={value} onChange={(e) => onChange(e.target.value)} disabled={productOptions.length === 0} > <option value="">Select Product</option> {productOptions.map((product, index) => ( <option key={index} value={product}> {product} </option> ))} </select> )} /> {/* 颜色选择框 */} <Controller control={control} name={`${name}.colorName`} defaultValue="" render={({ field: { onChange, value } }) => ( <select className="select" value={value} onChange={(e) => onChange(e.target.value)} disabled={colorOptions.length === 0} > <option value="">Select Colors</option> {colorOptions.map((color, index) => ( <option key={index} value={color}> {color} </option> ))} </select> )} /> </div> ); };
3. 添加表单默认值
在CategoryAdd的useForm中配置对应字段的默认值:
const { register, handleSubmit, formState: { isSubmitting }, reset, control, } = useForm({ defaultValues: { order: { categoryName: '', productName: '', colorName: '' } } });
关键修改说明
- 使用
useController获取分类字段状态,通过useEffect监听分类变化联动更新产品和颜色选项。 - 每个下拉框通过
Controller绑定到表单状态,确保选择变化同步到表单数据。 - 将空选项值改为空字符串
"",避免提交无效数值类型数据。 - 添加
disabled属性,无选项时禁用产品和颜色选择框,优化用户体验。
内容的提问来源于stack exchange,提问作者Tony
相关产品推荐
相关产品推荐

