Tools
React JSON Schema Forms in Practice — Why They Break Down and How SurveyJS Fixes the Architecture
2026-01-15
0 views
admin
The Core Architectural Difference ## React JSON Schema Forms ## SurveyJS ## Conditional Logic: When Schema Mutation Meets React State ## The Problem in react-jsonschema-form ## How SurveyJS Handles Conditional Logic ## Validation: Why Errors Disappear in Schema-Based React Forms ## The Problem in RJSF (and Uniforms) ## How SurveyJS Handles Validation ## Schema Updates on State Change: The Hidden Cost of "Dynamic Schemas" ## Performance with Large Schemas: When React Becomes the Bottleneck ## SurveyJS Architecture: Why This Works ## When Product Teams Choose SurveyJS ## Final Thought React JSON schema forms promise something very attractive to product teams: define your form once as JSON, render it anywhere, and let the schema handle validation, defaults, and conditional logic. Libraries like @rjsf/core (react-jsonschema-form) and Uniforms deliver on that promise for small to medium forms. But once forms become dynamic, state-dependent, or large, teams start hitting the same architectural limits again and again. This article explains why those problems occur and how SurveyJS Form Library approaches the problem differently and separates business logic from rendering by introducsing survey-core, a platform-independent survey model for SurveyJS Form Library and survey-react-ui, a React-specific UI rendering package. Before diving into specific issues, it's important to understand the root cause. React JSON Schema libraries treat the schema as both structure and behavior. They rely on schema mutation (dependencies, oneOf, dynamically regenerated schemas) and depend heavily on React re-renders to apply logic. Most of the problems below stem directly from that distinction. SurveyJS takes a different approach: JSON is treated as a declarative model, business logic is encapsulated in the form engine (survey-core), and React is used only for rendering, not orchestration. In RJSF, conditional fields are typically implemented using dependencies, oneOf, or dynamically regenerated schemas. This works, but only as long as the schema never changes shape unexpectedly. In React JSON Schema forms, the schema is not just a description of fields - it is the form itself. Using dependencies, oneOf, or anyOf dynamically switches schema branches at runtime. From React's perspective, fields are added or removed, the internal form tree changes shape, and previously mounted inputs may unmount and remount. This is manageable when the schema is static, conditionals are simple, and no external data influences the schema. But real product forms often need feature flags, backend-driven configuration, role-based visibility, and progressive disclosure. As soon as the schema is modified dynamically (for example, regenerated in a useEffect), React sees it as a new form definition, not a continuation of the previous one. That's when input state, defaults, and validation can reset in surprising ways. React JSON Schema form libraries also rely on React's render cycle to coordinate behavior. A field change updates formData, triggering a re-render, which may cause a different schema branch to be selected and then validated. This chain only works when updates happen in perfect order. React may batch updates or re-render components opportunistically, which can lead to flickering fields, disappearing validation messages, or inputs losing focus. None of this is a React bug—it is a consequence of encoding business logic into the render cycle. Real-world forms often rebuild schemas in useEffect to handle multiple fields, derived values, or async data. This leads to lost state, broken validation, and brittle logic. SurveyJS does not mutate schemas to express behavior. Instead, conditions are runtime expressions evaluated by survey-core. The key idea is that the schema stays static: the form's structure is defined once and treated as a durable contract rather than something that changes in response to user interaction. Fields are never removed or recreated; they always exist in the model, giving the engine a stable reference point. Visibility is evaluated at runtime by the engine, making field appearance deterministic and independent of React render timing. React is never asked to rebuild the form. It simply renders what the engine reports as visible or active. State changes trigger rule evaluation, not structural mutation, eliminating the need for schema regeneration, deep equality checks, or defensive memoization. This separation allows SurveyJS to handle complex conditional logic, large schemas, and frequent state changes without becoming fragile. Conditional logic in SurveyJS is therefore predictable, composable, and scalable across even very large forms. Validation in React JSON Schema forms is typically delegated to Ajv and mapped back into React state. Teams often encounter issues such as disappearing errors after schema updates, async validation racing with renders, and complex cross-field validation requiring custom plumbing. The underlying problem isn't Ajv—it's that validation is tied to React render cycles. Any schema or state change risks resetting the error tree. SurveyJS treats validation as a first-class engine concern: Validation runs independently of React, errors are persisted in the survey model, and cross-field or async validation are built-in. Moving validation out of React removes the need to sequence setState calls, debounce validation manually, or reconcile async results with rendering. Complex rules stay readable because they are declared close to the fields they affect, rather than scattered across component logic. In RJSF, changing the schema usually means replacing the schema object, often in a useEffect: This pattern causes full form reinitialization, loss of user input, validation resets, and performance degradation. Teams often add memoization, deep merges, or schema diffing to compensate, which adds complexity. SurveyJS avoids schema updates entirely. Instead, the schema is static and behavior changes—such as conditional visibility, required fields, or validation—are handled through runtime expressions. React simply renders what the engine reports as active. This separation ensures predictable behavior, maintains user input, preserves validation state, and improves performance for large forms. Large JSON schemas in React forms often hit serious performance limits: initial renders are slow, deep comparisons on re-renders are expensive, and single-field updates can trigger unnecessary renders elsewhere. Uniforms and RJSF perform well for small forms but struggle at scale. SurveyJS was designed for large forms from the ground up. It uses incremental rendering, dependency-based updates, and lazy loading of pages and questions. React only renders currently visible questions, and only affected fields re-evaluate, which dramatically reduces unnecessary re-renders and improves performance. Behind the scenes, the engine handles visibility conditions, dynamic rules, and validation in real time, without diffing the entire schema. You can explore a real-world demo here: Content-Heavy JSON Forms. SurveyJS clearly separates business logic from rendering: React itself is never responsible for evaluating conditions, managing validation state, or coordinating schema changes. You can read more here: SurveyJS Architecture SurveyJS is a better fit when forms are long-lived, conditional logic is central, performance matters at scale, non-trivial validation is required, or schema mutation becomes a maintenance burden. React JSON Schema forms are still valuable, but SurveyJS is designed for what they struggle with most. Most issues attributed to "React JSON Schema form" aren't implementation bugs—they're architectural limits. SurveyJS solves these problems by moving form intelligence out of React, letting product engineers focus on features instead of fighting render cycles. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK:
const schema = { type: "object", properties: { hasCar: { type: "boolean" } }, dependencies: { hasCar: { oneOf: [ { properties: { hasCar: { const: true }, carModel: { type: "string" } } }, { properties: { hasCar: { const: false } } } ] } }
}; Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
const schema = { type: "object", properties: { hasCar: { type: "boolean" } }, dependencies: { hasCar: { oneOf: [ { properties: { hasCar: { const: true }, carModel: { type: "string" } } }, { properties: { hasCar: { const: false } } } ] } }
}; CODE_BLOCK:
const schema = { type: "object", properties: { hasCar: { type: "boolean" } }, dependencies: { hasCar: { oneOf: [ { properties: { hasCar: { const: true }, carModel: { type: "string" } } }, { properties: { hasCar: { const: false } } } ] } }
}; CODE_BLOCK:
{ "name": "carModel", "type": "text", "visibleIf": "{hasCar} = true"
} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
{ "name": "carModel", "type": "text", "visibleIf": "{hasCar} = true"
} CODE_BLOCK:
{ "name": "carModel", "type": "text", "visibleIf": "{hasCar} = true"
} COMMAND_BLOCK:
<Form schema={schema} validator={validator} onError={(errors) => console.log(errors)}
/> Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
<Form schema={schema} validator={validator} onError={(errors) => console.log(errors)}
/> COMMAND_BLOCK:
<Form schema={schema} validator={validator} onError={(errors) => console.log(errors)}
/> CODE_BLOCK:
{ "name": "email", "type": "text", "isRequired": true, "validators": [ { "type": "email" } ]
} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
{ "name": "email", "type": "text", "isRequired": true, "validators": [ { "type": "email" } ]
} CODE_BLOCK:
{ "name": "email", "type": "text", "isRequired": true, "validators": [ { "type": "email" } ]
} COMMAND_BLOCK:
const [schema, setSchema] = useState(baseSchema); useEffect(() => { if (formData.type === "advanced") { setSchema(advancedSchema); }
}, [formData]); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
const [schema, setSchema] = useState(baseSchema); useEffect(() => { if (formData.type === "advanced") { setSchema(advancedSchema); }
}, [formData]); COMMAND_BLOCK:
const [schema, setSchema] = useState(baseSchema); useEffect(() => { if (formData.type === "advanced") { setSchema(advancedSchema); }
}, [formData]); CODE_BLOCK:
import { Survey } from 'survey-react-ui'; const surveyJson = { /* ... */ } export default function SurveyComponent() { const survey = new Model(surveyJson); return <Survey model={survey} />;
} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
import { Survey } from 'survey-react-ui'; const surveyJson = { /* ... */ } export default function SurveyComponent() { const survey = new Model(surveyJson); return <Survey model={survey} />;
} CODE_BLOCK:
import { Survey } from 'survey-react-ui'; const surveyJson = { /* ... */ } export default function SurveyComponent() { const survey = new Model(surveyJson); return <Survey model={survey} />;
} - survey-core — a platform-independent survey model for SurveyJS Form Library
- survey-react-ui — a React-specific form renderer
- survey-creator-core — a platform-independent data model for SurveyJS Survey Creator, a drag-and-drop form builder
- survey-creator-react — a React-specific Survey Creator UI renderer
how-totutorialguidedev.toaiswitch