如何让Yup Schema读取React应用中其他位置的状态?
问题描述
背景
我正在学习React+TypeScript,做一个基础的待办事项列表应用。Yup校验规则写在单独的文件里,App.tsx里存着整个应用的状态,用createContext/useContext做属性透传,Formik+Yup的表单校验目前运行正常。
需求
想给Yup加个自定义校验:检查用户输入的项目名称是否已经在App.tsx的allProjects状态里存在,防止创建同名项目报错,但不知道怎么实现,也不确定有没有更简便的方案。
已尝试的代码
我试着通过Context获取数据,用Yup的test方法检查项目名是否在allProjects的键集合中,但代码无法正常运行:
import * as yup from "yup"; import parse from "date-fns/parse"; import React, {useContext} from "react"; import { AllContext } from "../App"; // 尝试读取App.tsx中声明的state const {allProjects, selectedProject, currentTask} = React.useContext(AllContext); const [allProjectsCopy, setAllProjects] = allProjects; let keys = Object.keys(allProjectsCopy); export const projectNameSchema = yup.object().shape({ projectName: yup .string() .min(2) .test('project-exists', 'Project already exists', value => !(keys.includes(value))) .required("Required"), });
解决方案
你的代码为啥跑不起来?
- Yup的Schema是静态创建的,在组件渲染前就会执行,而
useContext只能在React组件或自定义Hook内部调用,直接在Schema文件中使用useContext违反了React的Hook规则,根本无法正确获取allProjects状态。 - 就算侥幸拿到数据,
keys也是Schema创建时的旧数据快照,不会随着allProjects的状态更新同步,校验逻辑永远用的是过时数据。
正确实现方式
方法1:将Schema改为工厂函数,在组件内动态生成
把Schema定义成接收allProjects参数的函数,在组件中获取Context数据后再生成Schema,这样每次状态更新都会生成最新的校验规则:
// schema.ts import * as yup from "yup"; // 定义工厂函数,接收allProjects作为参数 export const createProjectNameSchema = (allProjects: Record<string, any>) => { const keys = Object.keys(allProjects); return yup.object().shape({ projectName: yup .string() .min(2) .test('project-exists', 'Project already exists', value => { if (!value) return true; // 空值交给required校验处理 return !keys.includes(value); }) .required("Required"), }); };
在使用Formik的组件中调用:
import { useContext } from "react"; import { Formik, Form, Field } from "formik"; import { AllContext } from "../App"; import { createProjectNameSchema } from "./schema"; const ProjectForm = () => { const { allProjects } = useContext(AllContext); const [allProjectsCopy] = allProjects; // 动态生成最新的Schema const projectNameSchema = createProjectNameSchema(allProjectsCopy); return ( <Formik initialValues={{ projectName: "" }} validationSchema={projectNameSchema} onSubmit={(values) => { // 处理提交逻辑 }} > {({ errors, touched }) => ( <Form> <Field name="projectName" placeholder="输入项目名" /> {errors.projectName && touched.projectName && <div className="error">{errors.projectName}</div>} <button type="submit">创建项目</button> </Form> )} </Formik> ); }; export default ProjectForm;
方法2:直接使用Formik的validate自定义校验函数
如果不想修改Yup Schema,也可以直接在Formik中使用validate属性,直接访问组件内的allProjects状态:
const ProjectForm = () => { const { allProjects } = useContext(AllContext); const [allProjectsCopy] = allProjects; const keys = Object.keys(allProjectsCopy); const validate = (values: { projectName: string }) => { const errors: { projectName?: string } = {}; if (!values.projectName) { errors.projectName = "必填项"; } else if (values.projectName.length < 2) { errors.projectName = "至少输入2个字符"; } else if (keys.includes(values.projectName)) { errors.projectName = "该项目名已存在"; } return errors; }; return ( <Formik initialValues={{ projectName: "" }} validate={validate} onSubmit={(values) => { // 处理提交逻辑 }} > {/* 表单内容和之前一致 */} </Formik> ); };
方案对比
- 方法1保留了Yup的链式校验风格,适合复杂表单场景,校验规则可复用。
- 方法2更简洁直接,适合简单校验场景,无需额外维护Schema函数。
内容的提问来源于stack exchange,提问作者manski
相关产品推荐
相关产品推荐

