- Published on
Formik源码解析
- Authors
- Name
- noodles
- 每个人的花期不同,不必在乎别人比你提前拥有
Formik是一个React表单方案,它学习成本和api复杂度相对简单.笔者工作中的项目就有基于Formik封装的表单方案,作为表单方案之旅的 第二弹,让我们一起看下Formik在表单方案上有哪些特点.
Formik简单使用
import { Formik, Form, Field } from 'formik';
const Basic = () => (
<div>
<h1>Anywhere in your app!</h1>
<Formik initialValues={{ name: '' }}>
<Form>
<Field name="name" type="text" />
</Form>
</Formik>
</div>
);
Formik源码解析
Formik在实现上可以分以下三部分看:
- Formik-入口函数 通过Provider的方式提供全局上下文
- Field-表单组件
- useFormik-Form全局上下文 包括校验逻辑/值更新,这后面的源码解析中也会主要关注值更新相关逻辑
Formik入口源码解析
export function Formik(props) {
const formikbag = useFormik(props);
const { component, children, render, innerRef } = props;
// 通过innerRef暴露全局store
React.useImperativeHandle(innerRef, () => formikbag);
return (
<FormikProvider value={formikbag}>
{component
? React.createElement(component, formikbag)
: render
? render(formikbag)
: children // children come last, always called
? isFunction(children)
? children(formikbag)
: !isEmptyChildren(children)
? React.Children.only(children)
: null
: null}
</FormikProvider>
);
}
Formik入口组件的渲染方式有如下三种方式:
- 通过component方式渲染
- 通过render-props方式渲染,这种方式已经被废弃了
- 渲染chidlren这是常用的方式,这里有当children是函数和非函数的区分.函数式的方式会导致form组件每次都重复渲染,推荐直接使用非函数式的方式渲染
Field源码解析
在表单字段上,Filed主要通过name字段实现与Form全局store的关联,从而实现字段更新校验.
export function Field({
// xxx props
validate,
as: is, // `as` is reserved in typescript lol
}) {
const {
...formik
} = useFormikContext();
const { registerField, unregisterField } = formik;
// 注册字段
React.useEffect(() => {
registerField(name, {
validate: validate,
});
return () => {
unregisterField(name);
};
}, [registerField, unregisterField, name, validate]);
// 获取当前字段相关的属性
const field = formik.getFieldProps({ name, ...props });
// 省略若干代码 比如支持component渲染模式/函数式children渲染/render props渲染
const asElement = is || 'input';
// 默认渲染input,可以传入select等
if (typeof asElement === 'string') {
const { innerRef, ...rest } = props;
return React.createElement(
asElement,
{ ref: innerRef, ...field, ...rest, className },
children
);
}
// 渲染自定义组件同时传入field相关属性
return React.createElement(asElement, { ...field, ...props, className }, children);
}
Field的代码功能也不多,主要就是注册字段将字段相关的属性传入组件内.字段的值改变的回调函数就是在这个阶段注入的.
useFormik源码解析
useFormik是Formik的全局Store,它提供组件注册/字段更新/校验等能力.这里主要从onChange函数回调入手看下整个表单的更新逻辑.
在Field组件中通过formik.getFieldProps就将handleChange函数传递给组件了.
const getFieldProps = React.useCallback(
(nameOrOptions: string | FieldConfig<any>): FieldInputProps<any> => {
const isAnObject = isObject(nameOrOptions);
const name = isAnObject
? (nameOrOptions as FieldConfig<any>).name
: nameOrOptions;
const valueState = getIn(state.values, name);
const field: FieldInputProps<any> = {
name,
value: valueState,
// onChange函数注入
onChange: handleChange,
onBlur: handleBlur,
};
// 省略若干代码
return field;
},
[handleBlur, handleChange, state.values]
);
// handleChange支持
// 直接处理事件和代码
// 直接设置值的方式-handleChange('fieldName')(newValue)
const handleChange = useEventCallback(
(eventOrPath) => {
if (isString(eventOrPath)) {
return event => executeChange(event, eventOrPath);
} else {
executeChange(eventOrPath);
}
}
);
handleChange函数触发之后,会调用到setFieldValue,最后会走进dispatch更新函数
const dispatch = React.useCallback((action: FormikMessage<Values>) => {
const prev = stateRef.current;
stateRef.current = formikReducer(prev, action);
// 当reducer处理后 前后值有变化的 触发Form重新渲染
if (prev !== stateRef.current) setIteration(x => x + 1);
}, []);
Formik的代码实现上相当简单,甚至可以把它简单理解为一个useReducer维护的store更新功能.它没有类似rc-filed-form的依赖更新/字段级别更新的能力/Form Group的联动能力等. 甚至在性能优化上都需要用户去考虑.但是简单也许才是必杀技,在一般性的业务场景似乎Formik的实现已经够用了,在社区上也有一些能跟Formik结合的组件库组件库.