React多步表单useFormContext解构control为null报错排查修复
运行项目抛出错误:TypeError: Cannot destructure property 'control' of 'Object(...)(...)' as it is null
开发React多步表单时耗时2天排查、查阅相关资料未解决。需求为实现表单必填校验:字段未输入内容时输入框标红,提示用户必填。当前代码运行直接抛出上述空值解构错误,校验功能无法正常生效。
组件上下文引入代码
import { multiStepContext } from '../StepContext'; export default function BasicStep() { const { setStep, userData, setUserData, firstName, lastName, setfirstName, setlastName, username, title, cnicNo, gender, dob, referredBy, // country, cityId, state, profilePicture, setUsername, settitle, setcnicNo, setDob, // setCountry, // setState, setprofilePicture, setcityId, countryId, setcountryId, setreferredBy, stateId, setstateId, setGender } = useContext(multiStepContext); const { control, formState: { errors } } = useFormContext();
表单字段实现代码
<Controller control={control} name='firstName' rules={{ required: true // type: 'email' }} render={({ field }) => ( <TextField label='First Name' value={userData[firstName]} onChange={(e) => setfirstName(e.target.value)} margin='normal' defaultValue={userData[firstName]} fullWidth variant='outlined' {...field} error={errors.firstName} />
下一步按钮代码
<Button onClick={() => setStep(2)} variant='contained' color='primary'> Next </Button>
useFormContext() 是react-hook-form提供的上下文钩子,只有组件被FormProvider包裹时才能拿到表单实例返回值,未包裹时返回值为null,直接解构null的control属性就会触发当前报错。
除此之外现有代码还存在两个会导致校验失效的问题:
- Controller的render函数中重复绑定value、onChange、defaultValue属性,和
{...field}自带的表单绑定逻辑冲突,会导致表单状态不同步 - 下一步按钮直接触发步骤切换,没有先调用表单校验方法,就算修复了上下文问题,未填字段也不会阻断跳转
1. 上层组件添加FormProvider包裹
在多步表单的最外层容器组件(管理step切换、持有全局状态的父组件)中,调用useForm创建表单实例,用FormProvider包裹所有子步骤组件,将表单实例透传给下层子组件:
import { useState } from 'react'; import { useForm, FormProvider } from 'react-hook-form'; import { multiStepContext } from './StepContext'; import BasicStep from './steps/BasicStep'; // 引入其他步骤组件 export default function MultiStepFormContainer() { // 创建表单实例,统一管理所有字段状态和校验规则 const formMethods = useForm({ defaultValues: { firstName: '', lastName: '', username: '', // 其余表单字段默认值在这里统一声明 }, mode: 'onBlur' // 失焦时就触发校验,体验更好 }); const [step, setStep] = useState(1); // 其余原有multiStepContext的状态逻辑保留 return ( <multiStepContext.Provider value={{ setStep, // 其余原有上下文传值保留 }}> {/* 所有步骤组件必须放在FormProvider内部 */} <FormProvider {...formMethods}> {step === 1 && <BasicStep />} {/* {step === 2 && <SecondStep />} 其余步骤按需渲染 */} </FormProvider> </multiStepContext.Provider> ) }
如果多步表单的步骤是独立路由页面,必须将FormProvider放在路由组件外层,不能随着路由切换卸载FormProvider,否则子组件依然拿不到表单上下文。
2. 修正表单字段绑定逻辑
删除重复的value、onChange、defaultValue绑定,表单状态统一交给react-hook-form管理,不需要单独为每个字段维护上下文状态,同时补充错误提示文案展示:
<Controller control={control} name='firstName' rules={{ required: '请输入First Name' // 配置必填提示文案 }} render={({ field, fieldState: { error } }) => ( <TextField label='First Name' margin='normal' fullWidth variant='outlined' {...field} error={!!error} // 存在错误时输入框标红 helperText={error?.message} // 展示对应错误提示 /> )} />
3. 修正下一步按钮跳转逻辑
点击按钮时先触发表单校验,校验通过后再执行步骤跳转:
首先在BasicStep组件中从useFormContext解构出handleSubmit方法:
const { control, formState: { errors }, handleSubmit } = useFormContext();
然后修改按钮点击事件:
<Button onClick={handleSubmit(() => { // 所有字段校验通过后才会执行跳转逻辑 setStep(2) })} variant='contained' color='primary' > Next </Button>
内容的提问来源于stack exchange,提问作者Hamzacodev

