FormikPatternsSkill formik-patterns

Formik表单处理与验证模式。用于构建表单,实现验证或处理表单提交。

前端开发 0 次安装 0 次浏览 更新于 3/3/2026

Formik Patterns

基本表单设置

import { useFormik } from 'formik';
import * as yup from 'yup';

const validationSchema = yup.object({
  email: yup.string().email('无效的邮箱').required('邮箱是必填项'),
  password: yup.string().min(8, '最少8个字符').required('密码是必填项'),
});

const LoginForm = () => {
  const formik = useFormik({
    initialValues: {
      email: '',
      password: '',
    },
    validationSchema,
    onSubmit: async (values) => {
      await loginMutation({ variables: { input: values } });
    },
  });

  return (
    <VStack gap="$4">
      <Input
        label="邮箱"
        value={formik.values.email}
        onChangeText={formik.handleChange('email')}
        onBlur={formik.handleBlur('email')}
        error={formik.touched.email ? formik.errors.email : undefined}
        keyboardType="email-address"
        autoCapitalize="none"
      />

      <Input
        label="密码"
        value={formik.values.password}
        onChangeText={formik.handleChange('password')}
        onBlur={formik.handleBlur('password')}
        error={formik.touched.password ? formik.errors.password : undefined}
        secureTextEntry
      />

      <Button
        onPress={formik.handleSubmit}
        isDisabled={!formik.isValid || formik.isSubmitting}
        isLoading={formik.isSubmitting}
      >
        登录
      </Button>
    </VStack>
  );
};

验证模式

常见模式

import * as yup from 'yup';

// 邮箱
email: yup.string()
  .email('无效的邮箱地址')
  .required('邮箱是必填项')

// 带要求的密码
password: yup.string()
  .min(8, '至少8个字符')
  .matches(/[a-z]/, '必须包含小写字母')
  .matches(/[A-Z]/, '必须包含大写字母')
  .matches(/[0-9]/, '必须包含数字')
  .required('密码是必填项')

// 确认密码
confirmPassword: yup.string()
  .oneOf([yup.ref('password')], '密码必须匹配')
  .required('请确认密码')

// 电话号码
phone: yup.string()
  .matches(/^\+?[1-9]\d{1,14}$/, '无效的电话号码')
  .required('电话是必填项')

// 可选字段,存在时验证
website: yup.string()
  .url('必须是一个有效的URL')
  .nullable()

// 数字范围
quantity: yup.number()
  .min(1, '最少1')
  .max(100, '最多100')
  .required('数量是必填项')

// 数组最少项
tags: yup.array()
  .of(yup.string())
  .min(1, '至少选择一个标签')

条件验证

const schema = yup.object({
  hasCompany: yup.boolean(),
  companyName: yup.string().when('hasCompany', {
    is: true,
    then: (schema) => schema.required('公司名称是必填项'),
    otherwise: (schema) => schema.nullable(),
  }),
});

表单字段助手

输入助手

const getFieldProps = (name: keyof typeof formik.values) => ({
  value: formik.values[name],
  onChangeText: formik.handleChange(name),
  onBlur: formik.handleBlur(name),
  error: formik.touched[name] ? formik.errors[name] : undefined,
});

// 使用
<Input label="邮箱" {...getFieldProps('email')} />

选择/选择器助手

<Select
  label="国家"
  value={formik.values.country}
  onValueChange={(value) => formik.setFieldValue('country', value)}
  error={formik.touched.country ? formik.errors.country : undefined}
  options={countryOptions}
/>

带有GraphQL的表单提交

const CreateItemForm = () => {
  const [createItem] = useCreateItemMutation({
    onCompleted: () => {
      toast.success({ title: '项目已创建' });
      navigation.goBack();
    },
    onError: (error) => {
      console.error('createItem failed:', error);
      toast.error({ title: '创建项目失败' });
    },
  });

  const formik = useFormik({
    initialValues: { name: '', description: '' },
    validationSchema,
    onSubmit: async (values, { setSubmitting }) => {
      try {
        await createItem({ variables: { input: values } });
      } finally {
        setSubmitting(false);
      }
    },
  });

  return (
    <VStack gap="$4">
      {/* 表单字段 */}
      <Button
        onPress={formik.handleSubmit}
        isDisabled={!formik.isValid || formik.isSubmitting}
        isLoading={formik.isSubmitting}
      >
        创建
      </Button>
    </VStack>
  );
};

带有初始值的编辑表单

const EditItemForm = ({ item }: { item: Item }) => {
  const [updateItem] = useUpdateItemMutation({
    onCompleted: () => toast.success({ title: '已保存' }),
    onError: (error) => {
      console.error('updateItem failed:', error);
      toast.error({ title: '保存失败' });
    },
  });

  const formik = useFormik({
    initialValues: {
      name: item.name,
      description: item.description ?? '',
    },
    enableReinitialize: true, // 当item属性变化时更新
    validationSchema,
    onSubmit: async (values) => {
      await updateItem({
        variables: { id: item.id, input: values },
      });
    },
  });

  // 跟踪表单是否有更改
  const hasChanges = formik.dirty;

  return (
    <VStack gap="$4">
      {/* 表单字段 */}
      <Button
        onPress={formik.handleSubmit}
        isDisabled={!hasChanges || !formik.isValid || formik.isSubmitting}
        isLoading={formik.isSubmitting}
      >
        保存更改
      </Button>
    </VStack>
  );
};

表单状态助手

const {
  values,          // 当前表单值
  errors,          // 验证错误
  touched,         // 已触摸的字段
  isValid,         // 表单通过验证
  isSubmitting,    // 提交中
  dirty,           // 值与初始不同
  handleSubmit,    // 提交处理器
  handleChange,    // 变更处理器
  handleBlur,      // 失焦处理器
  setFieldValue,   // 设置单个字段
  setFieldTouched, // 标记字段触摸
  resetForm,       // 重置为初始值
  setSubmitting,   // 控制提交状态
} = formik;

多步骤表单

const MultiStepForm = () => {
  const [step, setStep] = useState(0);

  const formik = useFormik({
    initialValues: {
      // 第1步
      name: '',
      email: '',
      // 第2步
      address: '',
      city: '',
      // 第3步
      cardNumber: '',
    },
    validationSchema: stepSchemas[step],
    onSubmit: async (values) => {
      if (step < steps.length - 1) {
        setStep(step + 1);
      } else {
        await submitOrder(values);
      }
    },
  });

  return (
    <VStack>
      {step === 0 && <PersonalInfoStep formik={formik} />}
      {step === 1 && <AddressStep formik={formik} />}
      {step === 2 && <PaymentStep formik={formik} />}

      <HStack gap="$4">
        {step > 0 && (
          <Button variant="outline" onPress={() => setStep(step - 1)}>
            返回
          </Button>
        )}
        <Button
          onPress={formik.handleSubmit}
          isDisabled={!formik.isValid}
          isLoading={formik.isSubmitting}
        >
          {step < steps.length - 1 ? '下一步' : '提交'}
        </Button>
      </HStack>
    </VStack>
  );
};

反模式

// 错误 - 没有显示验证错误
<Input
  value={formik.values.email}
  onChangeText={formik.handleChange('email')}
/>

// 正确 - 触摸时显示错误
<Input
  value={formik.values.email}
  onChangeText={formik.handleChange('email')}
  onBlur={formik.handleBlur('email')}
  error={formik.touched.email ? formik.errors.email : undefined}
/>


// 错误 - 提交按钮始终启用
<Button onPress={formik.handleSubmit}>提交</Button>

// 正确 - 无效或提交时禁用
<Button
  onPress={formik.handleSubmit}
  isDisabled={!formik.isValid || formik.isSubmitting}
  isLoading={formik.isSubmitting}
>
  提交
</Button>


// 错误 - 突变时没有错误处理
onSubmit: async (values) => {
  await createItem({ variables: { input: values } });
}

// 正确 - 处理错误
onSubmit: async (values, { setSubmitting }) => {
  try {
    await createItem({ variables: { input: values } });
  } catch (error) {
    toast.error({ title: '保存失败' });
  } finally {
    setSubmitting(false);
  }
}

与其他技能的集成

  • graphql-schema: 突变提交模式
  • react-ui-patterns: 加载/错误状态
  • testing-patterns: 测试表单验证和提交