在 Expo / React Native 中使用 TanStack Form:Standard Schema 驱动的移动端表单实战

📅 发布时间:2026/9/17 6:37:55
在 Expo / React Native 中使用 TanStack Form:Standard Schema 驱动的移动端表单实战
在 Expo / React Native 中使用 TanStack FormStandard Schema 驱动的移动端表单实战【免费下载链接】form Headless, performant, and type-safe form state management for TS/JS, React, Vue, Angular, Solid, and Lit.项目地址: https://gitcode.com/GitHub_Trending/form/form本篇技术指南基于当前仓库中的官方 Expo 示例项目examples/react/expo系统讲解如何在一个由create-expo-app生成的 Expo 应用中安装、启动并使用 TanStack Form 构建类型安全的移动端表单。该示例的核心场景是用 Standard Schema 标准统一接入 Zod、Valibot、ArkType、Effect 四种校验库并在真机、模拟器与 Web 端复用同一套表单逻辑。读完本文你将掌握 Expo 项目的依赖安装与启动方式、文件路由组织、TanStack Form 的useForm/form.Field/form.Subscribe三大核心 API以及 Standard Schema 校验在底层form-core源码的识别与执行机制。示例项目概览一个“可跑起来的” Standard Schema 表单仓库中的examples/react/expo是一个基于 Expo SDK 54 的 React Native 应用。它的入口代码app/index.tsx实现了一个姓名表单firstName与lastName两个输入框配合同一套校验规则最少 3 个字符、firstName以字母A开头提交前自动拦截无效数据。整个表单由tanstack/react-formv1.33.5驱动校验规则直接使用 Standard Schema 兼容的 schema 对象。项目目录结构如下examples/react/expo/ ├── app/ │ └── index.tsx # 表单实现文件路由的首页 ├── assets/images/ # 图标、启动屏、Android 自适应图标等静态资源 ├── scripts/ │ └── reset-project.js # Expo 模板自带的“重置项目”脚本 ├── app.json # Expo 应用配置插件、路由、自适应图标 ├── eslint.config.js ├── metro.config.js # 针对 pnpm monorepo 的 Metro 打包配置 ├── package.json └── tsconfig.json # 基于 expo/tsconfig.base 的严格模式配置从依赖清单package.json可以看出这个示例并非单纯演示而是一个完整的集成环境表单核心tanstack/react-form^1.33.5校验库全家桶zod^3.25.76、valibot^1.1.0、arktype^2.1.22、effect^3.17.14移动端运行时expo~54.0.33、react-native0.81.5、react19.1.0、react-native-web~0.21.0支撑 Web 端输出路由与导航expo-router~6.0.23 及其依赖react-navigation/native等。第一步安装依赖进入示例目录后执行安装命令npm install该示例位于 pnpm 管理的 monorepo 中仓库根目录包含 pnpm-workspace.yamltanstack/react-form等依赖同时存在于仓库根部的node_modules/.pnpm与示例自身的node_modules。安装完成后即可进入下一步。第二步启动应用在示例目录下运行npx expo start启动成功后Expo 会在终端输出可用的打开方式你可以选择以下任意一种运行目标说明development build使用expo run:android/expo run:ios构建的原生开发版适合使用需要原生模块的功能Android emulator启动 Android 模拟器需安装 Android Studio 与 AVDiOS simulator启动 iOS 模拟器仅 macOSExpo Go在手机上安装 Expo Go 客户端扫码运行适合快速体验针对不同目标package.json 还预置了对应的脚本npm run android # 等价于 expo start --android npm run ios # 等价于 expo start --ios npm run web # 等价于 expo start --web以 Web 方式打开 npm run lint # 运行 expo lint 做静态检查其中npm run web之所以可用是因为项目安装了react-native-web且 app.json 中web.output被配置为static可以输出静态 Web 页面——这也意味着同一份表单代码可以在 iOS、Android 与浏览器三端复用。文件路由在 app 目录中开发页面示例使用 Expo Router 的文件路由约定app目录下的每个文件即一个路由页面。app/index.tsx对应应用首页入口字段在 package.json 中通过main: expo-router/entry指向。此外app.json 中开启了两个对路由和编译有影响的实验特性experiments: { typedRoutes: true, reactCompiler: true }typedRoutes为路由生成 TypeScript 类型跳转路径具备编译期检查reactCompiler启用 React Compiler 进行自动 memo 优化。核心实现一个可切换四种校验库的 Standard Schema 表单打开 app/index.tsx可以看到完整的表单实现。它复用了仓库中 Web 版示例 examples/react/standard-schema/src/index.tsx 的表单逻辑并针对 React Native 做了组件替换TextInput/Button/StyleSheet。1. 定义四种 schema示例同时用四种库声明等价的校验规则方便对比 Standard Schema 的接入方式// Zod const ZodSchema z.object({ firstName: z .string() .min(3, [Zod] You must have a length of at least 3) .startsWith(A, [Zod] First name must start with A), lastName: z.string().min(3, [Zod] You must have a length of at least 3), }) // Valibot const ValibotSchema v.object({ firstName: v.pipe( v.string(), v.minLength(3, [Valibot] You must have a length of at least 3), v.startsWith(A, [Valibot] First name must start with A), ), lastName: v.pipe( v.string(), v.minLength(3, [Valibot] You must have a length of at least 3), ), }) // ArkType const ArkTypeSchema type({ firstName: string 3, lastName: string 3, }) // Effect const EffectSchema S.standardSchemaV1( S.Struct({ firstName: S.String.pipe( S.minLength(3), S.annotations({ message: () [Effect/Schema] You must have a length of at least 3, }), ), lastName: S.String.pipe( S.minLength(3), S.annotations({ message: () [Effect/Schema] You must have a length of at least 3, }), ), }), )四种声明方式风格迥异对象链式、管道式、字符串 DSL、Schema 组合但因为它们都实现了 Standard Schema 接口接入 TanStack Form 的方式完全一致。2. 接入 useForm只改一行即可切换校验库useForm接收defaultValues、validators与onSubmitconst form useForm({ defaultValues: { firstName: , lastName: , }, validators: { // DEMO: You can switch between schemas seamlessly onChange: ZodSchema, // onChange: ValibotSchema, // onChange: ArkTypeSchema, // onChange: EffectSchema, }, onSubmit: async ({ value }) { // Do something with form data console.log(value) }, })注释明确展示了“无缝切换”的用法取消注释哪一行表单就采用哪种校验库其余代码零改动。validators.onChange表示在每次值变化时触发校验onSubmit收到校验通过后的value。3. 渲染字段form.Field 的 render props 模式每个输入框由form.Field组件渲染通过 children 函数拿到字段实例form.Field namefirstName children{(field) ( Text style{styles.label}First Name:/Text TextInput style{styles.input} value{field.state.value} onBlur{field.handleBlur} onChangeText{(text) field.handleChange(text)} autoCapitalizenone autoCorrect{false} / FieldInfo field{field} / / )} /field.state.value读取当前值field.handleChange更新值并触发校验field.handleBlur记录触摸touched状态。注意 React Native 的TextInput使用onChangeText而非 Web 的onChange这是移动端接入时唯一的差异点。4. 展示校验状态FieldInfo 组件示例抽出了一个FieldInfo组件统一展示错误与校验中状态function FieldInfo({ field }: { field: AnyFieldApi }) { return ( View {field.state.meta.isTouched !field.state.meta.isValid ? ( Text style{styles.error} {field.state.meta.errors.map((err) err.message).join(,)} /Text ) : null} {field.state.meta.isValidating ? TextValidating.../Text : null} /View ) }field.state.meta.isTouched字段是否被触碰过避免一进入页面就报错field.state.meta.isValid当前是否通过校验field.state.meta.errors错误数组元素的message即 schema 库给出的错误文案field.state.meta.isValidating是否正在异步校验。5. 提交与禁用form.Subscribe 响应式订阅提交按钮通过form.Subscribe精确订阅表单级状态只有canSubmit为 true 时才可点击form.Subscribe selector{(state) [state.canSubmit, state.isSubmitting]} children{([canSubmit, isSubmitting]) ( Button title{isSubmitting ? ... : Submit} disabled{!canSubmit} onPress{() form.handleSubmit()} / )} /form.handleSubmit()触发onSubmit当任意字段未通过校验时canSubmit为 false按钮自动禁用提交期间按钮文案变为...。底层原理Standard Schema 校验是如何被识别与执行的为什么四种库的 schema 对象能直接塞进validators答案在form-core包的 standardSchemaValidator.ts 中。Standard Schema 约定任何实现该标准的 schema 对象都带有一个~standard属性。form-core据此做鸭子类型检测export const isStandardSchemaValidator ( validator: unknown, ): validator is StandardSchemaV1 !!validator ~standard in (validator as object)在字段校验的执行路径 FieldApi.ts 中TanStack Form 会先判断传入的校验器是否是标准 schemaif (isStandardSchemaValidator(props.validate)) { return standardSchemaValidatorsprops.type as never } return (props.validate as FieldValidateFnany, any)(props.value) as never若是标准 schema走standardSchemaValidators的统一校验通道validate同步 /validateAsync异步否则按普通校验函数处理。standardSchemaValidators.validate的核心是调用schema[~standard].validate(value)并根据validationSource是field还是form返回不同结构的错误const result schema[~standard].validate(value) if (result instanceof Promise) { throw new Error(async function passed to sync validator) } if (!result.issues) return if (validationSource field) { return result.issues as TStandardSchemaValidatorIssueTSource } return transformFormIssuesTSource(result.issues, value)值得注意的实现细节同步校验路径如果遇到返回 Promise 的 schema会抛出async function passed to sync validator避免同步/异步混用表单级校验validationSource form时prefixSchemaToErrors会根据 issue 的path把错误按字段路径如firstName、lastName、数组下标[0]分组便于表单级错误定位。这套机制同样服务于FormApi与FormGroupApi见 FormApi.ts、FormGroupApi.ts因此标准 schema 校验在字段级、表单级、字段组级全部可用。与 Web 版示例的对应关系将 app/index.tsx 与 examples/react/standard-schema/src/index.tsx 对比可以发现两者的useForm配置、四种 schema 定义、FieldInfo逻辑几乎一一对应。区别仅在于渲染层能力Web 版standard-schema移动版expo输入组件input onChange{...}TextInput onChangeText{...}提交组件button typesubmitButton onPress{...}错误展示em标签Text style{styles.error}这说明 TanStack Form 的表单状态管理与校验逻辑与 UI 框架解耦同一套校验规则可以在 Web 与 React Native 之间原样复用——这正是本仓库主打 headless 架构的直接体现。获取一个全新项目reset-projectExpo 模板自带了reset-project脚本当你希望从空白项目重新开始开发时在示例目录执行npm run reset-project脚本scripts/reset-project.js的行为如下交互式询问是否把现有代码移到app-example目录输入Y还是直接删除输入n将app、components、hooks、constants、scripts等模板目录移动/清理生成全新的app目录内含最小化的index.tsx与_layout.tsx一个Stack根布局作为你新应用的起点。注意该脚本会删除或移动你已有的app目录建议在确认示例运行无误后再执行或直接基于现有app/index.tsx继续开发。monorepo 环境适配Metro 配置要点该示例位于 pnpm monorepo 中直接运行会遇到符号链接与多份 React 实例等问题metro.config.js 专门做了适配值得在同类仓库中复用config.watchFolders [ path.resolve(monorepoRoot, packages), path.resolve(monorepoRoot, node_modules/.pnpm), ] config.resolver.unstable_enableSymlinks true config.resolver.unstable_enablePackageExports true config.resolver.nodeModulesPaths [ path.resolve(projectRoot, node_modules), path.resolve(monorepoRoot, node_modules), path.resolve(monorepoRoot, node_modules/.pnpm/node_modules), ]watchFolders让 Metro 同时监听packages目录与 pnpm 的.pnpm存储保证tanstack/react-form的本地源码改动能热更新enableSymlinks / nodeModulesPaths解决 pnpm 符号链接与依赖解析顺序问题extraNodeModules 单例固定将react、react-native、expo、expo-router等强制指向示例自身node_modules防止多实例导致的 hooks 失效或原生模块重复注册最终用wrapWithReanimatedMetroConfig包裹兼容react-native-reanimated4.x。同时tsconfig.json 基于expo/tsconfig.base开启strict模式并配置/*路径别名保证整个示例在严格类型检查下运行。小结通过examples/react/expo这个示例你可以完整走通“Expo 项目初始化 → 安装依赖 → 启动应用 → 接入 TanStack Form → 无缝切换 Zod / Valibot / ArkType / Effect 四种校验库”的全流程。其中useForm统一管理状态、form.Field提供类型安全的字段绑定、form.Subscribe实现细粒度的响应式订阅而form-core的isStandardSchemaValidator检测与standardSchemaValidators通道则是“一套校验规则多库复用”的底层保障。如果你需要在移动端与 Web 端维护同一套表单校验这个示例就是最直接的参考模板。【免费下载链接】form Headless, performant, and type-safe form state management for TS/JS, React, Vue, Angular, Solid, and Lit.项目地址: https://gitcode.com/GitHub_Trending/form/form创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考