Formik 2.x 迁移指南:从 v1 升级的破坏性变更、新特性与弃用警告全解析
【免费下载链接】formikBuild forms in React, without the tears 😭项目地址: https://gitcode.com/gh_mirrors/fo/formik
本指南以 Formik 仓库根目录的 MIGRATING-v2.md(与 packages/formik/MIGRATING-v2.md 内容一致)为骨架,系统梳理从 Formik 1.x 升级到 2.x 时必须注意的破坏性变更、2.x 引入的核心新特性(复选框/多选内建绑定、useField、<Field as>、prop getters 等)以及官方弃用警告。读完本文,你将掌握resetForm、setStatus、validatePromise 语义、TypeScript 泛型调整等迁移要点,并能直接套用文中的迁移后代码在 React 16.8+ 项目中落地。
适用前提:Formik 2 完全基于 React Hooks 构建,要求 React 16.8.x 及以上;同时由于 2.x 内部使用了
unknown类型,使用 TypeScript 时需为 3.0 及以上版本。
一、破坏性变更(Breaking Changes)
Formik 2.x 引入的破坏性变更数量有限,官方明确表示"这些改动大概率不会影响太多人",但每一处都会在特定场景下改变既有行为,升级时必须逐一核对。
1. 最低环境要求
- React 16.8.x 或更高:Formik 2 构建在 React Hooks 之上,低于该版本无法运行。
- TypeScript 3.0 或更高(如果使用 TypeScript):因为 Formik 2 使用了
unknown类型,旧版 TS 无法编译。
这一约束与 useFormik 完全基于React.useRef、React.useCallback、React.useEffect等 Hooks 的实现直接相关,从源码结构看,整个表单状态机(stateRef+formikReducer)都依赖 Hooks 提供的能力。
2.resetForm:签名从"仅重置 values"变为"重置完整初始状态"
由于 2.x 新增了initialErrors、initialTouched、initialStatus三个 props,resetForm的签名随之改变——它现在接收的是Formik 的下一份初始状态(而不是仅仅下一个初始 values)。
v1 写法:
resetForm(nextValues);v2 写法:
resetForm({ values: nextValues /* errors, touched, etc ... */ });在源码中,resetForm的类型被定义在 types.tsx:
resetForm: (nextState?: Partial<FormikState<Values>>) => void;其实现位于 Formik.tsx:当未传入nextState.values时回退到initialValues.current,errors、touched、status也分别回退到initialErrors.current、initialTouched.current、initialStatus.current;若传入onReset回调,则先执行onReset(支持返回 Promise),再派发RESET_FORMaction 更新整个状态树(isSubmitting、isValidating、submitCount也会按nextState或默认值重置)。因此:
- 不带参数调用
resetForm()会重置回全部初始状态; - 带
{ values: nextValues }调用则只覆盖 values,其余仍取初始值; - 你也可以传入
errors、touched、status、isSubmitting、submitCount等任意字段做细粒度重置。
3.setError:请改用setStatus(status)
v1 中的setError在 2.x 中已移除,官方建议直接使用setStatus(status),两者行为完全一致。setStatus在 types.tsx 中定义,实现为向 reducer 派发SET_STATUSaction(见 Formik.tsx),用于手动设置表单顶层的status字段(例如提交失败时的服务端错误)。
4.validate:拒绝(reject)的 Promise 不再被当作校验错误
v1 中你可以从validate返回一个校验错误的 Promise,并且不区分 resolve 还是 reject——两种情况都会把 Promise 的载荷解释为校验错误对象。2.x 改变了这一语义:
- reject 的 Promise 会被当作真实异常,不会更新表单的 errors 状态,只在非生产环境打印一条警告;
- 所有返回"拒绝态错误 Promise"的校验函数,都必须改为返回 resolve 的错误对象 Promise。
源码层面,Formik.tsx 的runValidateHandler明确定义了这一行为:Promise 走.then(errors => resolve(errors || emptyErrors)),而 rejection 分支reject(actualException)并输出Warning: An unhandled error was caught during validation in <Formik validate />。注意:这只会影响validate函数本身——通过validationSchema(Yup)产生的ValidationError会被yupToFormErrors捕获并正常转为错误对象(见 Formik.tsx),不受此变更影响。
5.ref:暂不支持直接挂到<Formik>
2.x 中仍无法通过refprop 直接获取 Formik 实例,官方提供innerRefprop 作为替代方案(React.forwardRef支持处于 WIP 状态)。<Formik>组件在 Formik.tsx 中通过React.useImperativeHandle(innerRef, () => formikbag)将完整的 formikbag 暴露给传入的innerRef,因此需要命令式调用表单方法时,应使用innerRef。
6. TypeScript 类型变更
FormikActions更名为FormikHelpers
迁移非常简单,直接改名或通过 import alias 兼容旧代码:
v1:
import { FormikActions } from 'formik';v2:
import { FormikHelpers as FormikActions } from 'formik';在 types.tsx 中,FormikHelpers<Values>汇总了setStatus、setErrors、setSubmitting、setTouched、setValues、setFieldValue、setFieldError、setFieldTouched、validateForm、validateField、resetForm、submitForm、setFormikState等全部命令式方法,它们会作为第二个参数传给onSubmit/onReset。
FieldProps现在接受两个泛型参数
FieldProps的两个泛型参数都可选,但FormValues从第一个参数移动到了第二个参数:
v1:
type Props = FieldProps<FormValues>;v2:
type Props = FieldProps<FieldValue, FormValues>;源码定义见 Field.tsx:
export interface FieldProps<V = any, FormValues = any> { field: FieldInputProps<V>; form: FormikProps<FormValues>; // if ppl want to restrict this for a given form, let them. meta: FieldMetaProps<V>; }第一个泛型V对应字段值类型,第二个FormValues对应整个表单的 values 类型。如果你同时需要约束两者,务必按新顺序书写。
二、2.x 新增特性(What's New)
1. 复选框与多选(Checkboxes 和 Select multiple)的内建数组绑定
这是 Formik 1.x 中最令人困惑的点之一:2.x 像 Angular、Vue、Svelte 一样,"修复"了 React 复选框与多选下拉框,提供内建的数组绑定与布尔值行为。
其底层实现在 Formik.tsx 的getFieldProps中:
- 普通复选框(无
valueprop):field.checked = !!valueState,值存储为布尔值; - 同 name 的复选框组(带不同
value):field.checked = Array.isArray(valueState) && ~valueState.indexOf(valueProp),同时field.value = valueProp——Formik 自动把勾选的 value 绑定到同一个数组,增删逻辑全部替你完成; - radio:
field.checked = valueState === valueProp; <select multiple>:field.value = field.value || []并设置field.multiple = true。
对应的写入侧逻辑位于executeChange(Formik.tsx)和getValueForCheckbox(Formik.tsx):当前值为布尔时返回Boolean(checked);当前值为数组时执行添加/移除;getSelectedValues(Formik.tsx)负责从多选下拉中提取选中值数组。
完整示例(来自迁移文档,可直接运行):
import React from 'react'; import { Formik, Field, Form } from 'formik'; import { Debug } from './Debug'; const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); const CheckboxExample = () => ( <div> <h1>Checkboxes</h1> <p> This example demonstrates how to properly create checkboxes with Formik. </p> <Formik initialValues={{ isAwesome: false, terms: false, newsletter: false, jobType: ['designer'], location: [], }} onSubmit={async values => { await sleep(1000); alert(JSON.stringify(values, null, 2)); }} > {({ isSubmitting, getFieldProps, handleChange, handleBlur, values }) => ( <Form> {/* This first checkbox will result in a boolean value being stored. */} <div className="label">Basic Info</div> <label> <Field type="checkbox" name="isAwesome" /> Are you awesome? </label> {/* Multiple checkboxes with the same name attribute, but different value attributes will be considered a "checkbox group". Formik will automagically bind the checked values to a single array for your benefit. All the add and remove logic will be taken care of for you. */} <div className="label"> What best describes you? (check all that apply) </div> <label> <Field type="checkbox" name="jobType" value="designer" /> Designer </label> <label> <Field type="checkbox" name="jobType" value="developer" /> Developer </label> <label> <Field type="checkbox" name="jobType" value="product" /> Product Manager </label> {/* You do not _need_ to use <Field>/useField to get this behavior, using handleChange, handleBlur, and values works as well. */} <label> <input type="checkbox" name="jobType" value="founder" checked={values.jobType.includes('founder')} onChange={handleChange} onBlur={handleBlur} /> CEO / Founder </label> {/* The <select> element will also behave the same way if you pass `multiple` prop to it. */} <label htmlFor="location">Where do you work?</label> <Field component="select" id="location" name="location" multiple={true} > <option value="NY">New York</option> <option value="SF">San Francisco</option> <option value="CH">Chicago</option> <option value="OTHER">Other</option> </Field> <label> <Field type="checkbox" name="terms" />I accept the terms and conditions. </label> {/* Here's how you can use a checkbox to show / hide another field */} {!!values.terms ? ( <div> <label> <Field type="checkbox" name="newsletter" /> Send me the newsletter <em style={{ color: 'rebeccapurple' }}> (This is only shown if terms = true) </em> </label> </div> ) : null} <button type="submit" disabled={isSubmitting}> Submit </button> <Debug /> </Form> )} </Formik> </div> ); export default CheckboxExample;要点回顾:
- 单个复选框(如
isAwesome、terms)直接绑定布尔值; - 同
name不同value的复选框自动聚合成数组(jobType),勾选/取消自动增删; - 不使用
<Field>/useField也可以:通过checked={values.jobType.includes('founder')}+handleChange/handleBlur获得相同行为; <select multiple>与复选框组行为一致,选中值同样绑定为数组(location);- 复选框还能驱动条件渲染(
terms为 true 时才显示newsletter)。
仓库中的 examples/checkboxes 与 examples/radio-group 目录提供了对应可运行示例。
2.useField():Hook 版的<Field>
useField就是"像<Field>一样的 Hook",详细用法见 docs/api/useField.md。其签名(Field.tsx):
export function useField<Val = any>( propsOrFieldName: string | FieldHookConfig<Val> ): [FieldInputProps<Val>, FieldMetaProps<Val>, FieldHelperProps<Val>]返回一个三元组:字段输入 props(value、name、checked、onChange、onBlur、multiple)、字段元数据(error、touched、initialValue、initialTouched、initialError)与命令式 helper(setValue、setTouched、setError)。传入字符串时会被规范化为{ name }对象(Field.tsx),并自动完成字段注册/注销(Field.tsx)。
import React from 'react'; import { useField } from 'formik'; function MyTextField(props) { // 返回适用于 <input /> 的字段 props const [field, meta, helpers] = useField(props.name); return ( <> <input {...field} {...props} /> {meta.error && meta.touched && <div>{meta.error}</div>} </> ); }FieldHookConfig是<Field>props 的子集:name(必填)、validate、type、multiple、value(仅 checkbox/radio 有效)。当配置对象中包含type: 'checkbox'、type: 'radio'、multiple: true等键时,useField返回的 props 会完全复刻<Field>的复选框/单选/多选行为。
3.useFormikContext():Hook 版的connect()
useFormikContext等价于 v1 的connect()HoC。它封装了React.useContext(FormikContext),并要求调用处必须是<Formik>的子组件(见 FormikContext.tsx):
export function useFormikContext<Values>() { const formik = React.useContext<FormikContextType<Values>>(FormikContext); invariant( !!formik, `Formik context is undefined, please verify you are calling useFormikContext() as child of a <Formik> component.` ); return formik; }同时,FormikContext、FormikProvider、FormikConsumer均已从 FormikContext.tsx 导出,并经由 index.tsx 对外暴露,这让深层次组件无需层层透传 props 即可访问表单状态与 helpers。
4.<Field as>:直接向组件/字符串标签注入 props
<Field>新增asprop,它会直接把onChange、onBlur、value等注入到目标组件或原生标签上。对使用 Emotion 或 styled-components 的开发者尤其友好——不再需要在包装函数里清理component的 render props。
// <input className="form-input" placeholder="Jane" /> <Field name="firstName" className="form-input" placeholder="Jane" /> // <textarea className="form-textarea"/> <Field name="message" as="textarea" className="form-textarea"/> // <select className="my-select"/> <Field name="colors" as="select" className="my-select"> <option value="red">Red</option> <option value="green">Green</option> <option value="blue">Blue</option> </Field> // with styled-components/emotion const MyStyledInput = styled.input` padding: .5em; border: 1px solid #eee; /* ... */ ` const MyStyledTextarea = MyStyledInput.withComponent('textarea'); // <input className="czx_123" placeholder="google.com" /> <Field name="website" as={MyStyledInput} placeholder="google.com"/> // <textarea placeholder="Post a message..." rows={5}></textarea> <Field name="message" as={MyStyledTextArea} placeholder="Post a message.." rows={4}/>源码实现见 Field.tsx:as被解构为is(因为as在 TypeScript 中是保留字),渲染优先级为render→ 函数式children→component→as(asElement = is || 'input',默认回退到<input>)。FieldConfig中as的类型定义支持字符串标签或任意 React 组件(含ForwardRefExoticComponent,见 Field.tsx)。
5. Misc 杂项更新
FormikContext已导出(见上文);validateOnMount?: boolean = false:新增 prop,默认关闭。开启后表单挂载完成即执行一次校验——源码中通过React.useEffect在validateOnMount为 true 且isMounted时调用validateFormWithHighPriority(initialValues.current)(见 Formik.tsx);配合enableReinitialize时,initialValues 变化也会触发重新校验(Formik.tsx);- 新增
initialErrors、initialTouched、initialStatusprops:分别用于指定表单初始错误、初始 touched、初始 status。它们通过React.useRef缓存(Formik.tsx),初始化时经cloneDeep进入 state(Formik.tsx),并作为FormikComputedProps只读暴露(types.tsx)。在enableReinitialize下,这些初始值变化会被同步回 state(Formik.tsx); - 顺带说明:
isInitialValid已在开发模式下打印弃用警告,官方建议改用initialErrors或validateOnMount(Formik.tsx)。
6.getFieldProps(nameOrProps):prop getter 风格取字段输入
FormikProps新增getFieldProps与getFieldMeta两个 prop getter(Kent C. Dodds 风格),适合偏好 prop drilling、未使用 context API,或需要自行构建自定义useField的场景。getFieldProps的返回类型定义于 types.tsx:
export interface FieldInputProps<Value> { /** Value of the field */ value: Value; /** Name of the field */ name: string; /** Multiple select? */ multiple?: boolean; /** Is the field checked? */ checked?: boolean; /** Change event handler */ onChange: FormikHandlers['handleChange']; /** Blur event handler */ onBlur: FormikHandlers['handleBlur']; }调用方式:传入字符串(字段名)或配置对象均可;传入对象时才会触发 checkbox/radio/multiple select 的特化处理(见上文getFieldProps实现 Formik.tsx)。
7.getFieldMeta(name):取字段元数据
给定字段名,返回该字段的计算元数据,类型定义于 types.tsx:
export interface FieldMetaProps<Value> { /** Value of the field */ value: Value; /** Error message of the field */ error?: string; /** Has the field been visited? */ touched: boolean; /** Initial value of the field */ initialValue?: Value; /** Initial touched state of the field */ initialTouched: boolean; /** Initial error message of the field */ initialError?: string; }实现见 Formik.tsx:value/error/touched分别从state.values/state.errors/state.touched中按路径取值(支持a.b.c这类嵌套路径,配合getIn),initialValue/initialTouched/initialError则取自三个 initial ref。这与useField返回的第二个元素完全一致,因此getFieldMeta也是自定义 Hook 或组件内部取元数据(如展示校验状态、样式切换)的常用入口。
三、弃用警告(Deprecation Warnings)
所有renderprops 均已弃用(控制台会打印警告)
对于<Field>、<FastField>、<Formik>、<FieldArray>,renderprop 已被弃用并伴随控制台警告,将在未来版本中移除,官方建议改用child callback function(子函数),这一设计是为了与 React Context Consumer 的用法对齐。
- <Field name="firstName" render={props => ....} /> + <Field name="firstName">{props => ... }</Field>源码中的对应警告可见于多处:
<Field render>的弃用警告与提示文案在 Field.tsx;<Formik render>的弃用警告在 Formik.tsx;FormikConfig.render与SharedRenderProps.render字段也均在类型定义上标注了@deprecated(types.tsx、types.tsx)。
注意<Field>的渲染优先级:render存在时仍会执行(向后兼容),随后依次是函数式children、component、as;同时源码会在开发环境对"as/component/render与函数式children混用"给出 invariant 警告(Field.tsx),迁移时应避免同时使用多种渲染方式。
四、迁移检查清单
升级到 Formik 2.x 时,建议按以下顺序逐项核对(均可在本仓库 packages/formik 目录下找到源码佐证):
- 确认 React ≥ 16.8、TypeScript ≥ 3.0;
- 全局搜索
resetForm(,将单参数调用改为resetForm({ values })或空参数; - 全局搜索
setError,改用setStatus; - 检查
validate函数是否返回 rejected Promise,一律改为 resolve; - 若通过
ref访问 Formik 实例,改用innerRef; - 将
FormikActions改为FormikHelpers,并按FieldProps<FieldValue, FormValues>的新顺序书写泛型; - 移除或替换所有
renderprop(控制台警告会逐个提示),统一使用子函数写法; - 可选:将 checkbox/多选逻辑简化——利用 2.x 的内建数组/布尔绑定替换手写的
checked/onChange胶水代码; - 可选:用
useField、useFormikContext、<Field as>、getFieldProps/getFieldMeta重构自定义输入组件,减少重复的 props 透传。
对于更详尽的 2.x API 说明,可继续阅读仓库中的 docs/api 文档(如 useFormik.md、Field.md、useField.md)与 packages/formik/CHANGELOG.md;若你还在 1.x 上并想了解 1.x 与 2.x 的行为差异,也可参考根目录 README.md 中关于 v2 的说明。
【免费下载链接】formikBuild forms in React, without the tears 😭项目地址: https://gitcode.com/gh_mirrors/fo/formik
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考