编辑详情
自定义组件参考:
实现自定义组件
在 formily 中使用自定义组件
const FormInput = connect(
Input,
mapProps((props, field) => {
return {
...props,
suffix: (
{field?.['loading'] || field?.['validating'] ? (
<LoadingOutlined />
) : (
props.suffix
</span>
}),
mapReadPretty(PreviewText.Input)
const DateSelect = observer(() => {
const parentField = useField();
return (
{}
<Field
name="date"
basePath={parentField.address}
component={[DatePicker]}
});
const NameInput = () => {
return (
<Field
name="firstName"
component={[FormInput]}
required
<Field
name="lastName"
component={[FormInput]}
required
const ConditionSelect = ({
value,
onChange
}) => {
onItemChange = (itemValue, itemIndex) => {
const newValule = [...value];
newValue[itemIndex] = {
...newValue[itemIndex],
value: itemValue,
onChange(newValue)
return (
<div>复杂的样式 & 数据逻辑</div>
{value?.map((item, index) => {
return (
<Input key={item.id} value={item.value} onChange={(e) => onItemChange(e, index)} />
</div>
const DemoForm = () => {
return (
<FormProvider form={form}>
{}
<Field
name="username"
title="用户名"
required
decorator={[FormItem]}
component={[FormInput]}
<ObjectField
name="name"
title="姓名"
decorator={[FormItem]}
component={[NameInput]}
{}
<Field
name="birthday"
title="生日"
required
decorator={[FormItem]}
component={[FormRadioGroup, {
options: [
label: <DateSelect />,
value: true,
label: '保密',
value: false,
}]}
<Field
name="conditionSelect"
title="条件选择"
required
decorator={[FormItem]}
component={[ConditionSelect]}
</FormProvider>
以上片段只是组织表单方式的一种,还有其他很多种方式大家可以参考官方文档
“叶子”字段值不使用 Field 管理状态的好处:
简单,“叶子”字段值可以在数据 onChange 时做很多复杂的工作
用不明白 formily 的状态管理时,可以使用自定义业务组件去控制数据状态
自定义业务组件可以实现很复杂的样式和复杂的数据结构
“叶子”字段值使用 Field 管理状态的好处:
可以使用 form.query 获取到字段的 Field 实例
可以使用 field effect hooks 进行字段监听
https://codesandbox.io/s/field-manage-data-9zmu5f?file=/App.tsx
显隐属性的读规则
继承逻辑:
如果父节点主动设置了 display 属性,子节点没有主动设置 display 属性,那么子节点会继承父节点的 display。
主动设置 display :
给字段配置了初始化属性 display/visible/hidden
如果初始化时没有配置,但是在后期又给字段设置了 display/visible/hidden
如果希望子节点从不继承变为继承,可以把 display 设置为 null。
form.query("test4").take()?.setDisplay("hidden");
console.log(form.query("test4.0")?.get('display'));
form.query("test4.0").take()?.setDisplay("none");
console.log(form.query("test4.0")?.get('display'));
form.query("test4.0").take()?.setDisplay(null);
console.log(form.query("test4.0")?.get('display'));
form.query("test4").take()?.setDisplay("none");
console.log(form.query("test4").get("display"));
console.log(form.query("test4").value());
console.log(form.query("test4.0").get("display"));
- field.setDisplay(‘none/visible/hidden’)
- field.display = ‘none/visible/hidden’
- field.setState((state) => state.diaplay = ‘none/visible/hidden’)
- field.visible = true/false
- field.setState((state) => state.visible = true/false)
- field.hidden = true/false
- field.setState((state) => state.hidden = true/false)
其中设置 visible 为 false 时 display 为 none :
https://github.com/alibaba/formily/blob/HEAD/packages/core/src/models/Form.ts#L382-L393
setValues = (values: any, strategy: IFormMergeStrategy = 'merge') => {
if (!isPlainObj(values)) return
if (strategy === 'merge' || strategy === 'deepMerge') {
this.values = merge(this.values, values, {
arrayMerge: (target, source) => source,
} else if (strategy === 'shallowMerge') {
this.values = Object.assign(this.values, values)
} else {
this.values = values as any
从源码可以看出,merge 和 deepMerge 都是用的同一种方式即深度合并赋值。
merge 策略源码:https://github.com/alibaba/formily/blob/HEAD/packages/shared/src/merge.ts#L132-L152
https://github.com/alibaba/formily/blob/formily_next/packages/core/src/models/Field.ts#L396-L402
set selfErrors(messages: FeedbackMessage) {
this.setFeedback({
type: 'error',
code: 'EffectError',
messages,
get selfErrors() {
return queryFeedbackMessages(this, {
type: 'error',
setFeedback = (feedback?: IFieldFeedback) => {
updateFeedback(this, feedback)
Field validator 结果转换为 feedbacks 的核心逻辑:
源码链接:https://github.com/alibaba/formily/blob/formily_next/packages/core/src/shared/internals.ts#L303-L324
export const validateToFeedbacks = async (
field: Field,
triggerType: ValidatorTriggerType = 'onInput'
) => {
const results = await validate(field.value, field.validator, {
triggerType,
validateFirst: field.props.validateFirst || field.form.props.validateFirst,
context: { field, form: field.form },
batch(() => {
each(results, (messages, type) => {
field.setFeedback({
triggerType,
type,
code: pascalCase(`validate-${type}`),
messages: messages,
} as any)
return results
patternType 不为 editable 或 visible 状态的不会进行 Field 校验
从源码中得出结论:
formily 这样设计的原因是:
为了防止用户修改校验结果污染本身校验器的校验结果,做严格分离,容易恢复现场。
更多阅读:formily 校验规则
实现联动逻辑
我们会遇到很多字段联动的表单需求,如 a 字段的修改需要联动 b 字段的修改,a 字段设置了特定的值后需要隐藏 c 字段等复杂的联动交互逻辑可能是表单中最复杂的部分了。一对一、一对多、链式、循环、异步、主动模式、被动模式等多种联动方式,稍不注意就会陷入了深坑当中。
demo示例:
const LeaveConfig: React.FC = observer(() => {
useFormEffects(() => {
onFieldValueChange('askForLeaveUnit.minUnit', (field) => {
const minUnit = field.value;
const minDurationField = field.query('minDuration').take() as FieldType;
if (minDurationField) {
const minDuration = minDurationField.value;
if (minDuration < minUnit) {
minDurationField.setValue(minUnit);
return;
minDurationField.validate();
});
});
return (
<Field
name="minUnit"
component={[
FormSelect,
size: 'md',
className: styles.formSmallSelect,
reactions={[
(field) => {
const leaveUnit: LeaveUnitItem = field.query('.leaveUnit').value();
if (!leaveUnit) {
return;
const newOptions = getScopeOptions(leaveUnit);
field.setComponentProps({
options: newOptions,
});
field.setValue(
Math.min(
Math.max(field.value, newOptions[0].value),
newOptions[newOptions.length - 1].value
- FormEffectHooks
- FieldEffectHooks
- setFormState
- setFieldState
常用的 fieldEffectHooks 有:
- onFieldValidateStart:监听某个字段校验触发开始的副作用钩子
其中,value change effect hooks:
- onFieldChange:用于监听某个字段的属性变化的副作用钩子,如监听字段的显隐变化
- onFieldValueChange:用于监听某个字段值变化的副作用钩子
- onFieldInitialValueChange:用于监听某个字段默认值变化的副作用钩子
- onFieldInputValueChange:用于监听某个字段 onInput 触发的副作用钩子
以上的 4 个 value change effect hooks 中最常用的为 onFieldValueChange 和 onFieldInputValueChange。
- onFieldReact 实现全局响应式逻辑
- FieldReaction 实现局部响应式逻辑
json schema 中的联动更多可以参考 SchemaReactions
SchemaReactions 实现 Schema 协议中的结构化逻辑描述(内部是基于 FieldReaction 来实现的)
一般情况下设置 field reaction 就能实现大部分功能。其中 field reaction 和 onFieldReact 内部都会自动收集依赖。最常见的问题有:
Q:为什么触发了 field 自身值改变调用了自身的 reaction?
* 最大的可能性是 field reaction 中使用了自身值,reaction 收集了自身值的依赖。
Q:reaction 中设置了需要监听某个 field value 的逻辑,但是并没有响应?
在创建 Field 实例时会建立 onFieldValueChange、onFieldInitialValueChange 等事件监听的连接,而 ObjectField 只会监听到对象属性的数量增减,ArrayField 只会监听到数组元素数量的改变
* 使用 ArrayField、ObjectField、VoidField 组件挂载字段值,但是这些组件在创建实例时没有设置子元素的 value change 监听,所以是无法监听到详细的字段值改变。
* 没有使用 Field 组件挂载字段值,无法收集到 Field 依赖,所以监听不到 value change。
* 监听的 Field 初始化时机有问题,reaction 初始化时,Field 还没有挂载,收集不到依赖。
* 监听的 Field path 错误,没有指向正确的 Field。
Q:组件显示隐藏后再显示 effect hooks 失效了
参考 formily issue:https://github.com/alibaba/formily/issues/1602
* formily 内部自动做了隐藏时 form dispose,为了避免内存泄漏的问题。
* createForm 中的 effects 可以使用 [addEffects api](https://core.formilyjs.org/zh-CN/api/models/form#addeffects) 或者 [useFormEffects hooks](https://react.formilyjs.org/zh-CN/api/hooks/use-form-effects) 方式替代。
* ⚠️ onFormReact 会在卸载的时候,收集的 Field 依赖项会分别执行 dispose 函数。
Q:被动模式容易进入死循环
* 多个值相互监听修改 value 可能会触发死循环,要注意循环判断。
主动模式 & 被动模式 demo:https://codesandbox.io/s/formily-reaction-demo-68zgri?file=/App.tsx
https://reactive.formilyjs.org/zh-CN/guide/concept#reaction
先从 reaction 的机制来看,reaction 在响应式编程模型中,它就相当于是可订阅对象的订阅者,它接收一个 tracker 函数,这个函数在执行的时候,如果函数内部有对 observable 对象中的某个属性进行读操作(依赖收集),那当前 reaction 就会与该属性进行一个绑定(依赖追踪),知道该属性在其他地方发生了写操作,就会触发 tracker 函数重复执行,用一张图表示:
可以看到从订阅到派发订阅,其实是一个封闭的循环状态机,每次 tracker 函数执行的时候都会重新收集依赖,依赖变化时又会重新触发 tracker 执行。
所以,如果一旦我们不想再订阅 reaction 了,一定要手动 dispose,否则会内存泄漏。
field reaction 和 effect hooks 中 formily 底层做了 dispose 的处理,所以一般使用时可以不用手动 dispose。
🥲 含泪总结的最佳实践(可能会有偏差,欢迎补充和讨论
- 表单字段的 reaction 太多了,可以全部抽到一个 reaction 的文件中,统一管理,记得注释。
- effect hooks 最好写到字段值定义的地方,就近原则。
- 一般情况下字段联动逻辑是主动模式和被动模式搭配使用,但是如果逻辑不清晰容易进入死循环,所以清晰的联动逻辑是很重要的。
- form effect hooks 会有显隐问题,所以需要使用 useMemo 控制创建 form 实例。
有时 Form 上挂载的组件变动过大,如某些情况下需要挂载 FormStep 组件,另外的情况又不需要挂载 FormStep 组件,如果一直使用同一个 form 实例就会出现问题,所以需要控制 useMemo 创建 form 实例的时机。
(不用 useMemo 可不可以?不可以,重复渲染会导致重复创建 form 从而引发更多的问题)
const formStep = useMemo(() => {
return FormStep.createFormStep(FormStepIndex.RULE_CONFIG);
}, [holidayTypeDetail?.holidayTypeId]);
const ruleDetailForm = useMemo(() => {
return createForm<RuleConfigForm>({
initialValues: RULE_CONFIG_FORM_INITIAL_VALUES,
});
}, [holidayTypeDetail?.holidayTypeId]);
ruleDetailForm.addEffects('leaveRuleDetailEffects', () => {
onFormValidateFailed(() => {
if (formStep.current === FormStepIndex.RULE_CONFIG) {
sendMessage({ id: ERROR_MESSAGE_ID, content: '基本规则配置有误', type: 'error' });
});
});
<FormProvider form={ruleDetailForm}>
{}
{!showSendRule ? (
<RuleConfig />
) : (
<VoidField
name="holidayRule"
component={[
FormStep.JsxFormStep,
className: styles.navWrap,
minWidth: 284,
formStep: formStep,
steps: [
title: '基本规则',
name: FormStepName.RULE_CONFIG,
content: <RuleConfig />,
title: '发假规则',
name: FormStepName.SEND_RULE_CONFIG,
content: <SendRuleConfig />,
</FormProvider>
官网的最佳实践:https://reactive.formilyjs.org/zh-CN/guide/best-practice
在使用@formily/reactive 的时候,我们只需要注意以下几点即可:
- 尽量少用 observable/observable.deep 进行深度包装,不是非不得已就多用 observable.ref/observable.shallow,这样性能会更好
- 领域模型中多用 computed 计算属性,它可以智能缓存计算结果
- 虽然批量操作不是必须的,但是尽量多用 batch 模式,这样可以减少 Reaction 执行次数
- 使用 autorun/reaction 的时候,一定记得调用 dispose 释放函数(也就是调用函数所返回的二阶函数),否则会内存泄漏
https://github.com/alibaba/formily/discussions/1928
batch:定义批量操作,内部可以收集依赖
interface batch {
<T>(callback?: () => T): T
scope<T>(callback?: () => T): T
bound<T extends (...args: any[]) => any>(callback: T, context?: any): T
endpoint(callback?: () => void): void
action:定义一个批量动作。与 batch 的唯一差别就是 action 内部是无法收集依赖的
interface action {
<T>(callback?: () => T): T
scope<T>(callback?: () => T): T
bound<T extends (...args: any[]) => any>(callback: T, context?: any): T
formliy 官网:https://formilyjs.org/zh-CN/guide
formily github:https://github.com/alibaba/formily
@formily/reactive:https://reactive.formilyjs.org/
@formily/core: https://core.formilyjs.org/
@formily/react: https://react.formilyjs.org/
formily 2.0 更新概要:https://github.com/alibaba/formily/discussions/1087
可能是你见过最专业的表单方案—解密Formily2.0 - 掘金