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: 测试表单验证和提交