mcpField
F
formik-patterns

formik-patterns

github.com/chriswiles开发6.1k118 次安装42 周前更新
代码仓库

关于 formik-patterns

Formik Patterns 汇集使用 Formik 和 Yup 构建表单的代码模式,面向 React 开发者。内容涵盖校验、字段辅助函数、提交及多步骤表单。

主要功能

  1. 01Formik 表单初始化
  2. 02Yup 常见与条件校验
  3. 03输入和选择字段辅助函数
  4. 04GraphQL 表单提交
  5. 05编辑表单与初始值
  6. 06多步骤表单状态管理

适用场景

  1. 01构建含必填和格式校验的表单
  2. 02按条件校验表单字段
  3. 03提交 GraphQL 创建或更新操作
  4. 04组织多步骤表单

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/chriswiles/claude-code-showcase --skill formik-patterns

安装后在 Claude Code 中输入/formik-patterns确认已加载。

详细介绍

Formik 模式

基本表单设置

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

const validationSchema = yup.object({
  email: yup.string().email('Invalid email').required('Email is required'),
  password: yup.string().min(8, 'Min 8 characters').required('Password is required'),
});

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

  return (
    <VStack gap="$4">
      <Input
        label="Email"
        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="Password"
        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}
      >
        Login
      </Button>
    </VStack>
  );
};

验证架构

常见模式
import * as yup from 'yup';

// Email
email: yup.string()
  .email('Invalid email address')
  .required('Email is required')

// Password with requirements
password: yup.string()
  .min(8, 'Must be at least 8 characters')
  .matches(/[a-z]/, 'Must contain lowercase letter')
  .matches(/[A-Z]/, 'Must contain uppercase letter')
  .matches(/[0-9]/, 'Must contain number')
  .required('Password is required')

// Confirm password
confirmPassword: yup.string()
  .oneOf([yup.ref('password')], 'Passwords must match')
  .required('Please confirm password')

// Phone number
phone: yup.string()
  .matches(/^\+?[1-9]\d{1,14}$/, 'Invalid phone number')
  .required('Phone is required')

// Optional field with validation when present
website: yup.string()
  .url('Must be a valid URL')
  .nullable()

// Number with range
quantity: yup.number()
  .min(1, 'Minimum 1')
  .max(100, 'Maximum 100')
  .required('Quantity required')

// Array with minimum items
tags: yup.array()
  .of(yup.string())
  .min(1, 'Select at least one tag')
条件验证
const schema = yup.object({
  hasCompany: yup.boolean(),
  companyName: yup.string().when('hasCompany', {
    is: true,
    then: (schema) => schema.required('Company name 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,
});

// Usage
<Input label="Email" {...getFieldProps('email')} />
选择器/选取器辅助工具
<Select
  label="Country"
  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: 'Item created' });
      navigation.goBack();
    },
    onError: (error) => {
      console.error('createItem failed:', error);
      toast.error({ title: 'Failed to create item' });
    },
  });

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

  return (
    <VStack gap="$4">
      {/* Form fields */}
      <Button
        onPress={formik.handleSubmit}
        isDisabled={!formik.isValid || formik.isSubmitting}
        isLoading={formik.isSubmitting}
      >
        Create
      </Button>
    </VStack>
  );
};

使用初始值编辑表单

const EditItemForm = ({ item }: { item: Item }) => {
  const [updateItem] = useUpdateItemMutation({
    onCompleted: () => toast.success({ title: 'Saved' }),
    onError: (error) => {
      console.error('updateItem failed:', error);
      toast.error({ title: 'Save failed' });
    },
  });

  const formik = useFormik({
    initialValues: {
      name: item.name,
      description: item.description ?? '',
    },
    enableReinitialize: true, // Update when item prop changes
    validationSchema,
    onSubmit: async (values) => {
      await updateItem({
        variables: { id: item.id, input: values },
      });
    },
  });

  // Track if form has changes
  const hasChanges = formik.dirty;

  return (
    <VStack gap="$4">
      {/* Form fields */}
      <Button
        onPress={formik.handleSubmit}
        isDisabled={!hasChanges || !formik.isValid || formik.isSubmitting}
        isLoading={formik.isSubmitting}
      >
        Save Changes
      </Button>
    </VStack>
  );
};

表单状态辅助工具

const {
  values,          // Current form values
  errors,          // Validation errors
  touched,         // Fields that have been touched
  isValid,         // Form passes validation
  isSubmitting,    // Submit in progress
  dirty,           // Values differ from initial
  handleSubmit,    // Submit handler
  handleChange,    // Change handler
  handleBlur,      // Blur handler
  setFieldValue,   // Set single field
  setFieldTouched, // Mark field touched
  resetForm,       // Reset to initial values
  setSubmitting,   // Control submitting state
} = formik;

多步骤表单

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

  const formik = useFormik({
    initialValues: {
      // Step 1
      name: '',
      email: '',
      // Step 2
      address: '',
      city: '',
      // Step 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)}>
            Back
          </Button>
        )}
        <Button
          onPress={formik.handleSubmit}
          isDisabled={!formik.isValid}
          isLoading={formik.isSubmitting}
        >
          {step < steps.length - 1 ? 'Next' : 'Submit'}
        </Button>
      </HStack>
    </VStack>
  );
};

反模式

// WRONG - Not showing validation errors
<Input
  value={formik.values.email}
  onChangeText={formik.handleChange('email')}
/>

// CORRECT - Show errors when touched
<Input
  value={formik.values.email}
  onChangeText={formik.handleChange('email')}
  onBlur={formik.handleBlur('email')}
  error={formik.touched.email ? formik.errors.email : undefined}
/>


// WRONG - Submit button always enabled
<Button onPress={formik.handleSubmit}>Submit</Button>

// CORRECT - Disabled when invalid or submitting
<Button
  onPress={formik.handleSubmit}
  isDisabled={!formik.isValid || formik.isSubmitting}
  isLoading={formik.isSubmitting}
>
  Submit
</Button>


// WRONG - No error handling on mutation
onSubmit: async (values) => {
  await createItem({ variables: { input: values } });
}

// CORRECT - Handle errors
onSubmit: async (values, { setSubmitting }) => {
  try {
    await createItem({ variables: { input: values } });
  } catch (error) {
    toast.error({ title: 'Failed to save' });
  } finally {
    setSubmitting(false);
  }
}

与其他技能集成

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

评论

支持 Markdown