如何向自定义Hook传递Formik实例?现有实现求更优雅方案
我想把Formik的验证逻辑抽离到自定义Hook中,这个Hook需要返回Yup验证规则,但构建规则时依赖Formik上下文(formik bag)里的方法。尝试过在组件根节点获取Formik实例引用并传给Hook,但自定义Hook始终只能拿到初始的undefined;把ref存入state又会导致无限渲染循环。后来用useFormik+FormikProvider实现了功能,但出现了循环依赖问题:Formik实例依赖validationSchema,而validationSchema又依赖Formik实例。最后通过给自定义Hook加setFormik方法解决,但想知道有没有更优雅的方案。
初始无效实现:
export const MyView = () => { let formikRef: FormikProps<FormData> | undefined = undefined; const { validationSchema } = useValidationSchema(formikRef); ... return ( <Formik initialValues={initialValues} onSubmit={submitForm} enableReinitialize={true} validateOnChange={false} validateOnBlur={false} validationSchema={validationSchema} > {(formik) => { formikRef = formik; return ( <> ... </> ); }} </Formik> ); }
临时解决方案:
const { validationSchema, setFormik } = useValidationSchema(); const formik = useFormik<FormData>({ initialValues: initialValues, onSubmit: submitForm, enableReinitialize: true, validateOnChange: false, validateOnBlur: false, validationSchema: validationSchema, }); setFormik(formik);
方案1:在自定义Hook中直接使用useFormikContext
Formik提供的useFormikContext Hook可以让你在Formik上下文范围内的组件/自定义Hook中直接获取完整的Formik实例,无需手动传递引用,是最简洁的实现方式。
实现步骤:
- 调整自定义Hook,内部通过
useFormikContext获取Formik实例,再构建验证规则:
// useValidationSchema.ts import { useFormikContext } from 'formik'; import * as Yup from 'yup'; export const useValidationSchema = () => { const formik = useFormikContext<FormData>(); const validationSchema = Yup.object({ username: Yup.string() .required('必填') .test( 'unique-username', '用户名已存在', async (value) => { if (!value) return true; const isUnique = await checkUsernameUnique(value); if (!isUnique) { formik.setFieldError('username', '用户名已存在'); } return isUnique; } ), // 其他字段规则... }); return { validationSchema }; };
- 在Formik上下文范围内使用Hook:
确保调用useValidationSchema的代码处于<Formik>或<FormikProvider>包裹下,可通过拆分子组件实现:
export const MyView = () => { return ( <Formik initialValues={initialValues} onSubmit={submitForm} enableReinitialize={true} validateOnChange={false} validateOnBlur={false} > {/* 子组件处于Formik上下文内,可直接调用Hook */} <FormContent /> </Formik> ); }; const FormContent = () => { const { validationSchema } = useValidationSchema(); return ( <Form> <Field name="username" /> <ErrorMessage name="username" /> {/* 其他表单元素 */} </Form> ); };
方案2:让validationSchema成为接收Formik实例的函数
Formik的validationSchema属性支持传入函数,该函数会自动接收Formik bag(包含实例核心方法和属性)作为参数并返回Yup schema。利用这个特性可以完全避免循环依赖问题。
实现步骤:
- 自定义Hook返回构建schema的函数:
// useValidationSchema.ts import * as Yup from 'yup'; export const useValidationSchema = () => { const buildSchema = (formik) => { return Yup.object({ username: Yup.string() .required('必填') .test( 'unique-username', '用户名已存在', async (value) => { if (!value) return true; const isUnique = await checkUsernameUnique(value); if (!isUnique) { formik.setFieldError('username', '用户名已存在'); } return isUnique; } ), // 其他字段规则... }); }; return { buildSchema }; };
- 在Formik配置中直接传入该函数:
export const MyView = () => { const { buildSchema } = useValidationSchema(); return ( <Formik initialValues={initialValues} onSubmit={submitForm} enableReinitialize={true} validateOnChange={false} validateOnBlur={false} // Formik会自动传入自身实例到buildSchema中 validationSchema={buildSchema} > {(formik) => ( <Form> <Field name="username" /> <ErrorMessage name="username" /> {/* 其他表单元素 */} </Form> )} </Formik> ); };
这个方案彻底解决了依赖顺序问题:Formik初始化时才会调用buildSchema并传入自身实例,此时schema才会被构建。
方案3:用useFormik的validate方法替代validationSchema
如果验证逻辑更适合自定义函数而非Yup schema,可以直接使用Formik的validate属性,该函数同样会接收Formik bag作为参数,将验证逻辑抽到Hook中即可。
实现示例:
// useFormValidation.ts export const useFormValidation = () => { const validate = async (values, formik) => { const errors = {}; if (!values.username) { errors.username = '必填'; } else { const isUnique = await checkUsernameUnique(values.username); if (!isUnique) { errors.username = '用户名已存在'; } } // 其他字段验证... return errors; }; return { validate }; };
组件中使用:
export const MyView = () => { const { validate } = useFormValidation(); const formik = useFormik<FormData>({ initialValues: initialValues, onSubmit: submitForm, enableReinitialize: true, validateOnChange: false, validateOnBlur: false, validate: (values) => validate(values, formik), }); return ( <FormikProvider value={formik}> <Form> <Field name="username" /> <ErrorMessage name="username" /> </Form> </FormikProvider> ); };
内容的提问来源于stack exchange,提问作者wekso

