Skip to main content
TL;DR: FormEngine registers a custom React component with a single chained call — define(MyComponent, 'Name').props({...}).build() — and the component is immediately usable from JSON, works in the visual Designer, and participates in validation, data binding, and events without extra plumbing. Competing JSON-driven form libraries require more concepts (Formik: hooks + provider + JSX, RJSF: widgets registry + uiSchema override, SurveyJS: model class + serializer + factory registration) and most do not produce a portable JSON schema that a non-developer can edit visually. FormEngine is a React library that renders forms from JSON schemas. This page compares how each library lets you plug your own React component into a form — a core task any non-trivial product hits within the first week. Last reviewed: April 2026.

Why custom components matter

Every production form eventually needs something the stock components don’t cover — a signature pad, a branded currency input, a company-specific address picker, a dependency-injected product selector. The question is not whether you’ll write a custom component, it’s how much ceremony the library imposes before your component is a first-class citizen of the form. Three things separate easy from painful:
  1. How many concepts you have to learn before your component works end-to-end (registration, data binding, validation, events).
  2. Whether the component is serializable — can a JSON schema reference it, or is it tied to JSX?
  3. Whether a visual editor can use it — or is your custom component invisible to non-developers on the team?
FormEngine is built around these three questions. The comparison below quotes the exact registration code each library requires for a minimal valued component (an input that writes to form data and responds to change events).

Side-by-side: registering a custom input

Same component, same behavior, four libraries. Each snippet is the smallest working version the official docs show.

FormEngine

import { define, event, string } from '@react-form-builder/core'

const MyInput = ({ value, onChange }) => (
  <input value={value ?? ''} onChange={e => onChange(e.target.value)} />
)

export const myInput = define(MyInput, 'MyInput')
  .props({
    value: string.valued,
    onChange: event
  })
  .build()
That’s it. Add myInput.model to your view once (createView([myInput.model, ...])) and the component is now addressable from any JSON schema as "type": "MyInput", participates in validation, is rendered by the free MIT runtime, and — if you license Designer — appears in the visual editor’s component palette automatically. Concepts you had to learn: define, prop type builders (string, event), .valued for data binding, .build(). One import, one chain, one export.

Formik

import { useField } from 'formik'

export const MyInput = ({ label, ...props }) => {
  const [field, meta] = useField(props)
  return (
    <>
      {label && <label htmlFor={props.id || props.name}>{label}</label>}
      <input {...field} {...props} />
      {meta.touched && meta.error ? <div className="error">{meta.error}</div> : null}
    </>
  )
}

// Usage — must be inside <Formik> provider:
// <MyInput name="email" label="Email" />
Formik’s useField hook returns a 3-tuple of [field, meta, helpers] that you splat onto your input manually. Error display is your responsibility. The component is ordinary JSX — it cannot be referenced from a JSON schema, cannot be rendered by anything other than a JSX tree wrapped in a Formik provider, and cannot be used by a visual builder without writing a separate runtime. Concepts you had to learn: useField, the field/meta/helpers tuple, how to splat field onto a native element, manual error rendering, Formik provider requirement. Sources: Formik useField tutorial, A Primer on Custom Fields (codedaily.io).

react-jsonschema-form (RJSF)

import Form from '@rjsf/core'
import validator from '@rjsf/validator-ajv8'

const MyInput = (props) => (
  <input
    type="text"
    value={props.value ?? ''}
    required={props.required}
    onChange={e => props.onChange(e.target.value)}
  />
)

const widgets = { myInput: MyInput }

const schema = {
  type: 'object',
  properties: { email: { type: 'string' } }
}
const uiSchema = {
  email: { 'ui:widget': 'myInput' }
}

<Form schema={schema} uiSchema={uiSchema} widgets={widgets} validator={validator} />
RJSF splits the description into three artifacts: a data schema (JSON Schema), a uiSchema that tells the renderer which widget to use for each field, and a widgets registry passed as a prop. The JSON Schema spec drives data shape, so custom UI concerns live in the parallel uiSchema. Your widget is only addressable if uiSchema references it — and uiSchema is typically JSON, so the widget identifier is a string lookup, not a direct component reference. Concepts you had to learn: JSON Schema vs uiSchema split, widget naming, widget registry prop, ui:widget override syntax, separate validator import. Source: RJSF Custom Widgets and Fields.

SurveyJS

import { ElementFactory, Question, Serializer } from 'survey-core'
import { ReactQuestionFactory, SurveyQuestionElementBase } from 'survey-react-ui'

const CUSTOM_TYPE = 'my-input'

class QuestionMyInputModel extends Question {
  getType() { return CUSTOM_TYPE }
}

Serializer.addClass(
  CUSTOM_TYPE,
  [],
  () => new QuestionMyInputModel(''),
  'question'
)

ElementFactory.Instance.registerElement(CUSTOM_TYPE, (name) => {
  return new QuestionMyInputModel(name)
})

class SurveyQuestionMyInput extends SurveyQuestionElementBase {
  get question() { return this.questionBase }
  renderElement() {
    return (
      <input
        value={this.question.value ?? ''}
        onChange={e => this.question.value = e.target.value}
      />
    )
  }
}

ReactQuestionFactory.Instance.registerQuestion(CUSTOM_TYPE, (props) => {
  return React.createElement(SurveyQuestionMyInput, props)
})
SurveyJS requires a model class (extending Question), registration with Serializer.addClass, registration with ElementFactory.Instance.registerElement, a React wrapper class extending SurveyQuestionElementBase, and registration with ReactQuestionFactory.Instance.registerQuestion. The component is not a plain React function — it is a class rendered by SurveyJS’s internal question engine. This gives you hooks into the survey lifecycle, but it also means you cannot hand a senior React dev a component and have it “just work” in the form. Concepts you had to learn: Question base class, Serializer, ElementFactory, ReactQuestionFactory, SurveyQuestionElementBase, the difference between “element” registration and “question” registration, model-vs-view split, class components. Source: SurveyJS Custom Question Renderer.

API surface at a glance

FormEngineFormikRJSFSurveyJS
Concepts to learn for a minimal valued custom component1 (define)3 (useField, provider, manual error rendering)3 (widgets registry, uiSchema, ui:widget override)5+ (Question, Serializer, ElementFactory, ReactQuestionFactory, SurveyQuestionElementBase)
Imports required from the library2–3 (define + prop builders)1 (useField)2 (Form, validator)4–5 (core + react-ui classes)
Registration styleSingle chained builderNone (render in JSX)Pass widgets prop + uiSchema overrideMultiple factory registrations
Component formPure React functionPure React functionPure React functionClass extending SurveyQuestionElementBase
JSON-addressableYes ("type": "MyInput")No (JSX only)Yes (via uiSchema widget name)Yes (via custom question type)
Works in visual editorYes — Designer picks it up automaticallyNo (Formik has no visual editor)No (RJSF has no first-party visual editor)Yes — SurveyJS Creator, but requires extra propertyEditors wiring
Validation hook-upDeclarative via schemaManual (meta.error display is on you)Driven by JSON Schema validatorsPart of Question lifecycle
Runtime licenseMIT (free)MIT (free, unmaintained since 2023)Apache 2.0 (free)Commercial for Creator/visual editor; MIT for runtime

Lines of code to production-ready

Counting only what the official docs show as the minimum for a valued, validated, JSON-addressable custom input:
  • FormEngine: ~10 lines (React function + define().props().build()).
  • Formik: ~15 lines of JSX — but not JSON-addressable, so it doesn’t reach feature parity.
  • RJSF: ~20 lines plus you carry a second schema (uiSchema) alongside your data schema everywhere the form is referenced.
  • SurveyJS: ~40 lines across model class, serializer call, two factory registrations, and a React wrapper class — before you’ve wired up properties or validation.
The interesting number is not raw line count, it’s how many files and concepts a new team member has to touch to answer “why does this field exist and how do I change it?”. FormEngine collapses that to one definition per component. SurveyJS spreads it across five APIs that have to stay in sync.

Portability: the argument that gets forwarded to your architect

A plain useField-based Formik input lives in JSX. It cannot be serialized. Moving a form from one app to another means copying components, not just schemas. This is the reason teams building multi-tenant SaaS, white-label products, or form-configurable admin tools choose schema-driven libraries. Inside the schema-driven camp, portability still varies:
  • FormEngine. A custom component is a named entry in the view. Any JSON schema that references "type": "MyInput" renders correctly in any app that includes myInput.model in its view. The same schema works in the runtime and in the Designer — no parallel uiSchema, no widget ID translation.
  • RJSF. Schema and uiSchema are two artifacts. To use the schema elsewhere you must also ship the uiSchema and the widgets registry that matches the widget names uiSchema references. If the widget names change between apps, schemas break silently.
  • SurveyJS. JSON survey definitions reference your custom question by type string. The receiving app must register the same model class, serializer class, and React question class before it can render. Portability works, but the receiving app imports code, not just JSON.
FormEngine’s design is “component defined once, schema portable forever.” That’s the shareable line for an architect who is tired of forms that only work in the app they were born in.

What this means for adoption

If you are evaluating FormEngine against these libraries and custom components matter to your product — and in enterprise and SaaS contexts, they always do — the practical wins are:
  • Less ceremony per component. One function, one chain, done. Useful when you have ten custom fields, not one.
  • Components don’t fragment your team. Designers/PMs can edit forms in Designer, developers can edit forms in JSON, and both are looking at the same custom component registry. You don’t need to rebuild the form twice.
  • MIT runtime. The free Core package renders your custom components in production at zero cost. Designer is optional and only needed when non-developers should edit the forms visually.
The honest caveats:
  • Formik’s useField is genuinely minimal if you never need JSON schemas or a visual editor. For a single internal form, it’s fine. The cost shows up when the same form has to live in three places.
  • RJSF is the right answer if you are strictly bound to the JSON Schema standard for data validation and interop with non-React consumers. FormEngine uses its own UI schema, which is richer for form concerns but not a JSON Schema drop-in.
  • SurveyJS is the right answer if the product is literally a survey platform — scoring, respondent tracking, multi-page question logic. The heavier custom-component API is the price of the richer survey model.

Next steps

Questions or corrections? The source is on GitHub and the team answers in GitHub Discussions.
Last modified on April 16, 2026