如何在react-hook-form中基于schema生成动态watch()/useWatch()实现字段联动
你要避免全量监听的性能损耗,核心是放弃全局的watch(),改用useWatch并指定仅监听目标依赖字段,配合预处理schema提取依赖映射的方式就能实现动态生成监听逻辑,具体实现如下:
实现步骤
1. 预处理schema提取依赖映射
先递归遍历整个schema,把每个带联动依赖的字段的完整路径、它要监听的依赖字段的完整路径、联动规则提取出来生成映射表,避免渲染时每次重复遍历schema。
// 预处理schema生成依赖映射 const generateDependencyMap = (schema) => { const depMap = {} // 递归遍历schema节点 const traverse = (node, parentPath = '') => { if (node.type === 'object' && node.properties) { Object.entries(node.properties).forEach(([fieldKey, fieldConfig]) => { const fullFieldPath = parentPath ? `${parentPath}.${fieldKey}` : fieldKey // 提取带依赖配置的字段 if (fieldConfig.dependencies?.watching) { // 拼接依赖字段的完整路径(和当前字段同父级) const fullWatchingPath = parentPath ? `${parentPath}.${fieldConfig.dependencies.watching}` : fieldConfig.dependencies.watching depMap[fullFieldPath] = { ...fieldConfig.dependencies, watchingPath: fullWatchingPath } } // 继续递归子节点 traverse(fieldConfig, fullFieldPath) }) } } traverse(schema) return depMap }
这里要注意嵌套字段的路径拼接:比如contractorInformation下的contractorFavouriteAnimal字段的完整路径是contractorInformation.contractorFavouriteAnimal,它依赖的contractorName完整路径是contractorInformation.contractorName,不要直接用schema里写的短名,否则useWatch会找不到对应字段。
2. 渲染字段时按需调用useWatch
渲染每个表单项时,先查依赖映射表,如果当前字段有依赖配置,就单独调用useWatch仅监听它需要的依赖字段,再根据返回值计算禁用/显示状态。
import { useForm, useWatch } from 'react-hook-form' const FormRender = ({ schema }) => { const { control, register } = useForm() const depMap = generateDependencyMap(schema) // 递归渲染字段的函数 const renderField = (fieldConfig, fullFieldPath) => { const depConfig = depMap[fullFieldPath] let disabled = false if (depConfig) { // 仅监听当前字段需要的依赖字段 const watchingValue = useWatch({ control, name: depConfig.watchingPath, defaultValue: '' }) // 按你的业务逻辑计算禁用状态 disabled = !watchingValue || watchingValue.length === 0 } // 根据ui类型渲染对应组件 if (fieldConfig.ui === 'input') { return <input {...register(fullFieldPath)} disabled={disabled} /> } // 其他组件类型逻辑同理 } // 剩余递归渲染schema的逻辑按你的原有实现补充即可 }
这种方式只有被监听的依赖字段发生变更时,才会触发当前表单项的重渲染,完全不会受其他无关字段更新的影响,性能损耗降到最低。
补充优化方案
如果你的表单依赖字段不多,也可以先把所有需要监听的依赖字段去重后统一监听,代码更简洁,性能也远好于全量监听:
// 提取所有需要监听的字段并去重 const allWatchingFields = [...new Set(Object.values(depMap).map(item => item.watchingPath))] // 统一监听所有依赖字段 const watchingValues = useWatch({ control, name: allWatchingFields, defaultValue: {} })
如果存在单字段依赖多个其他字段的场景,把schema里的watching字段改成数组类型即可,useWatch的name参数原生支持传入数组批量监听多个字段,仍然只有这些字段变更时才会触发更新。
如果表单字段量级很大,可以把每个表单项拆成独立的子组件,进一步缩小重渲染的影响范围,性能会更好。
内容的提问来源于stack exchange,提问作者jjlittle

