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

如何向自定义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实例,无需手动传递引用,是最简洁的实现方式。

实现步骤:

  1. 调整自定义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 };
};
  1. 在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。利用这个特性可以完全避免循环依赖问题。

实现步骤:

  1. 自定义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 };
};
  1. 在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 11:12:23