React JSON Schema 表单实战 — 为什么它们会出现问题以及 SurveyJS 如何修复架构
Source: Dev.to
根本原因
React JSON Schema 库将 schema 同时视为 结构 和 行为。它们依赖 schema 的变更(依赖关系、oneOf、动态重新生成的 schema),并且在很大程度上依赖 React 的重新渲染来应用逻辑。下面的大多数问题直接源于这种混淆。
SurveyJS 采用不同的方法:JSON 被视为 声明式模型,业务逻辑封装在表单引擎(survey‑core)中,React 仅用于渲染,而不用于编排。
RJSF 中的条件字段
在 RJSF 中,条件字段通常使用 dependencies、oneOf 或动态重新生成的 schema 实现:
const schema = {
type: "object",
properties: {
hasCar: { type: "boolean" }
},
dependencies: {
hasCar: {
oneOf: [
{
properties: {
hasCar: { const: true },
carModel: { type: "string" }
}
},
{
properties: {
hasCar: { const: false }
}
}
]
}
}
};
这仅在 schema 从未意外改变形状 时有效。在 React JSON Schema 表单中,schema 不仅是字段的描述——它 就是 表单本身。使用 dependencies、oneOf 或 anyOf 会在运行时动态切换 schema 分支。从 React 的角度来看,字段被添加或移除,内部表单树的形状会改变,先前挂载的输入可能会卸载再重新挂载。
当满足以下条件时,这种方式是可管理的:
- schema 是静态的。
- 条件逻辑简单。
- 没有外部数据影响 schema。
然而,实际产品表单往往需要:
- 功能标记(feature flags)。
- 后端驱动的配置。
- 基于角色的可见性。
- 逐步披露(progressive disclosure)。
一旦 schema 被动态修改(例如在 useEffect 中重新生成),React 会将其视为一个 全新的 表单定义,而不是对先前表单的延续。输入状态、默认值和验证可能会以出人意料的方式被重置。
渲染周期耦合
React JSON Schema 表单库还依赖 React 的渲染周期来协调行为:
- 字段变化更新
formData。 - React 重新渲染。
- 可能会选择不同的 schema 分支并进行验证。
只有在更新严格按顺序进行时,这一链路才会正常工作。React 可能会批量更新或在合适的时机重新渲染组件,这会导致:
- 字段闪烁。
- 验证信息消失。
- 输入失去焦点。
这些都不是 React 的 bug,而是将业务逻辑编码进渲染周期所导致的后果。
SurveyJS:静态 Schema + 运行时表达式
SurveyJS 不 通过修改 schema 来表达行为。相反,条件是由 survey‑core 在运行时求值的表达式:
{
"name": "carModel",
"type": "text",
"visibleIf": "{hasCar} = true"
}
关键理念
- Schema 保持 静态——表单结构只定义一次,并被视为持久的契约。
- 字段从不被删除或重新创建;它们始终存在于模型中,为引擎提供稳定的引用点。
- 可见性由引擎在运行时求值,使字段的出现具有确定性且不受 React 渲染时机的影响。
React 从不需要重新构建表单。它只渲染引擎报告为可见或激活的部分。状态变化触发规则求值,而不是结构性变更,从而消除了 schema 重新生成、深度相等检查或防御性 memo 化的需求。这种分离使 SurveyJS 能够在处理复杂的条件逻辑、大型 schema 和频繁的状态变化时保持稳健。
因此,SurveyJS 中的条件逻辑 可预测、可组合且可扩展,即使在非常大的表单中也是如此。
验证
在 React JSON Schema 表单中,验证通常委托给 Ajv,并映射回 React 状态:
// Example JSX (simplified)
<Form
schema={schema}
onChange={...}
validate={...}
/>
团队经常会遇到以下问题:
- 在模式更新后错误消失。
- 异步验证与渲染竞争。
- 需要自定义管道的复杂跨字段验证。
根本问题不在于 Ajv——而是验证绑定在 React 的渲染周期上。任何模式或状态的更改都可能导致错误树被重置。
SurveyJS 验证
SurveyJS 将验证视为 一等引擎关注点:
{
"name": "email",
"type": "text",
"isRequired": true,
"validators": [
{ "type": "email" }
]
}
- 验证独立于 React 运行。
- 错误保存在调查模型中。
- 跨字段或异步验证已内置。
将验证移出 React 后,就不需要对 setState 调用进行排序、手动防抖验证,或将异步结果与渲染进行调和。复杂规则保持可读,因为它们声明在影响的字段附近,而不是分散在组件逻辑中。
动态模式重新生成 – 常见陷阱
在 RJSF 中,更改 schema 通常意味着 替换 schema 对象,通常在 useEffect 中:
const [schema, setSchema] = useState(baseSchema);
useEffect(() => {
if (formData.type === "advanced") {
setSchema(advancedSchema);
}
}, [formData]);
这种模式会导致 整个表单重新初始化、用户输入丢失以及验证失效。SurveyJS 通过保持 schema 静态并在引擎内部处理所有条件逻辑来避免这些问题。
摘要
| 方面 | React JSON Schema Forms (RJSF) | SurveyJS Form Library |
|---|---|---|
| Schema 角色 | 结构 以及 行为(已变更) | 纯声明式模型(静态) |
| Conditional logic | dependencies、oneOf、模式再生成 | 运行时表达式(visibleIf、enableIf 等) |
| State handling | 与 React 渲染周期绑定;可能导致卸载/重新挂载 | 引擎维护稳定的字段实例 |
| Validation | Ajv + React 状态;在模式更改时容易被重置 | 引擎驱动,持久化于模型 |
| Scalability | 在大型、动态表单中变得脆弱 | 可预测、可组合,适用于大型模式 |
通过将 JSON 视为 静态契约 并将业务逻辑迁移到专用引擎,SurveyJS 将表单的 what(外观) 与 how(行为) 分离。这带来了更可靠、可维护且可扩展的表单——尤其在处理复杂、动态或大规模应用时。
为什么 SurveyJS 在大型、动态表单上优于 React JSON‑Schema Forms
React JSON‑Schema 库的问题
- 模式变更 – 更新 JSON 模式(添加/删除字段、修改验证规则)会导致整个表单重新渲染。
- 状态丢失 – 当模式改变时,用户输入、验证状态以及 UI 状态常常会被重置。
- 性能下降 – 大型模式会导致首次渲染缓慢、深度对象比较开销大,以及不相关字段的无谓重新渲染。
开发者通常会通过记忆化、深度合并或模式差分来缓解这些问题,但这些变通方案会增加复杂度,且仍未根本解决问题。
SurveyJS 的做法
- 静态模式 – JSON 模式在运行时永不改变。
- 运行时表达式 – 条件可见性、必填逻辑和验证都以公式的形式由 SurveyJS 引擎求值。
- 关注点分离 –
- survey‑core – 与平台无关的模型,保存表单定义、状态和业务逻辑。
- survey‑react‑ui – 轻量的 React 包装层,只负责渲染引擎标记为“活跃”的内容。
因为 React 只负责渲染,它无需评估条件、管理验证状态或调和模式变更。这带来:
- 可预测的行为
- 在动态变化时保持用户输入和验证状态
- 对大型表单的显著性能提升
性能优势
| 问题 | React JSON‑Schema 库 | SurveyJS |
|---|---|---|
| 大型模式的首次渲染 | 缓慢(完整树渲染) | 快速(增量渲染) |
| 更新单个字段 | 可能触发整个表单重新渲染 | 仅重新评估受影响的字段 |
| 每次渲染的深度比较 | 开销大 | 被避免 – 引擎自行处理差分 |
| 懒加载页面/问题 | 很少支持 | 内置,仅渲染可见项 |
最小示例(在 React 中使用 SurveyJS)
import { Survey } from "survey-react-ui";
import { Model } from "survey-core";
const surveyJson = {
/* … your survey definition … */
};
export default function SurveyComponent() {
const survey = new Model(surveyJson); // 引擎处理逻辑
return <Survey model={survey} />; // React 只负责渲染 UI
}
幕后工作原理 – SurveyJS 引擎实时求值可见性条件、动态规则和验证,而无需对整个模式进行差分。
你会使用的包
- survey‑core – 与平台无关的调查模型(业务逻辑)。
- survey‑react‑ui – React 专用渲染器。
- survey‑creator‑core – 拖拽式 Survey Creator 的模型。
- survey‑creator‑react – Survey Creator 的 React UI。
了解更多架构信息:SurveyJS Architecture (link)。
何时选择 SurveyJS
- 表单 长期存在 并会随时间演进。
- 条件逻辑(显示/隐藏、启用/禁用、动态验证)是核心需求。
- 需要 大规模高性能(数百字段、多页调查)。
- 验证 复杂(自定义表达式、跨字段规则)。
- 可变的 JSON 模式已成为 维护负担。
React JSON‑Schema 表单仍然适用于简单、静态的表单,但在上述场景下,SurveyJS 提供了更清晰、更快且更易维护的解决方案。
资源
- 演示: Content‑Heavy JSON Forms – 一个展示 SurveyJS 处理大型、动态表单的实时示例。
- 文档:
大多数被归咎于“React JSON‑Schema 表单”的问题并非 bug,而是固有的架构限制。SurveyJS 将繁重的工作从 React 中抽离,使产品团队能够专注于功能,而不是与渲染周期作斗争。