Skip to main content
New to FormEngine? It’s a React library that renders forms from JSON schemas — free, MIT-licensed. Start here →
TL;DR: Formik stores form state in a React context and wires it through Field components and handleSubmit. FormEngine stores form structure in a JSON schema and renders it through <FormViewer>. The migration moves field definitions and validation rules from JSX/Yup into a JSON object, and replaces Formik’s onSubmit with onActionEventAsync. Last reviewed: April 2026. This guide maps Formik patterns to FormEngine equivalents with before/after code. Migration is incremental — Formik and FormEngine can coexist in the same app while you migrate form by form. FormEngine Core is open-source on GitHub. For the full architecture comparison, see FormEngine vs Formik.

When to migrate

Formik’s last major release was in 2021. The library is stable but sees minimal active development. Teams typically migrate when:
  • They want a library with active maintenance
  • Forms need runtime configuration without redeployment
  • Non-developers need to edit forms through a visual designer
  • They want to consolidate form state into a JSON schema stored in a database
If Formik is working and your forms are stable, there’s no urgency. If you’re starting a new project or adding new forms, FormEngine is worth evaluating.

Concept mapping

FormikFormEngine
useFormik({ initialValues, onSubmit })<FormViewer form={schema} onActionEventAsync={...}>
<Field name="x" />Component entry in JSON schema
<Form> / handleSubmitsubmit action on a Button
validationSchema (Yup)validations array on each component
<ErrorMessage name="x" />Built-in — FormEngine renders errors inline
values.xform.data.x in expressions
touched.xHandled internally by FormEngine
setFieldValuesetValues action
<FieldArray>Repeater component in schema
<FastField>Not needed — FormEngine re-renders efficiently by default

Before/after: basic form

Formik:

const validationSchema = Yup.object({
  firstName: Yup.string().required('First name is required'),
  email: Yup.string().email('Invalid email').required('Email is required'),
  message: Yup.string().min(10, 'At least 10 characters').required('Message is required'),
})

function ContactForm() {
  return (
    <Formik
      initialValues={{ firstName: '', email: '', message: '' }}
      validationSchema={validationSchema}
      onSubmit={async (values) => {
        await fetch('/api/contact', {
          method: 'POST',
          body: JSON.stringify(values),
        })
      }}
    >
      <Form>
        <Field name="firstName" placeholder="First name" />
        <ErrorMessage name="firstName" component="span" />

        <Field name="email" type="email" />
        <ErrorMessage name="email" component="span" />

        <Field name="message" as="textarea" />
        <ErrorMessage name="message" component="span" />

        <button type="submit">Send</button>
      </Form>
    </Formik>
  )
}
FormEngine:
{
  "version": "1",
  "form": {
    "name": "ContactForm",
    "type": "form",
    "components": [
      {
        "name": "firstName",
        "type": "Input",
        "props": { "label": "First name" },
        "validations": [
          { "type": "required", "message": "First name is required" }
        ]
      },
      {
        "name": "email",
        "type": "Input",
        "props": { "label": "Email" },
        "validations": [
          { "type": "required", "message": "Email is required" },
          { "type": "email", "message": "Invalid email" }
        ]
      },
      {
        "name": "message",
        "type": "Textarea",
        "props": { "label": "Message" },
        "validations": [
          { "type": "required", "message": "Message is required" },
          { "type": "minLength", "value": 10, "message": "At least 10 characters" }
        ]
      },
      {
        "name": "submit",
        "type": "Button",
        "props": { "label": "Send" },
        "actions": [{ "event": "onClick", "action": "submit" }]
      }
    ]
  }
}

function ContactForm() {
  return (
    <FormViewer
      view={view}
      form={JSON.stringify(schema)}
      onActionEventAsync={async (event) => {
        if (event.name === 'submit') {
          await fetch('/api/contact', {
            method: 'POST',
            body: JSON.stringify(event.args.formData),
          })
        }
      }}
    />
  )
}
Key difference: FormEngine renders error messages inline next to each field automatically — no <ErrorMessage> components needed.

Before/after: Yup validationSchema

Formik uses a top-level validationSchema that defines rules for all fields. FormEngine defines rules per-component in the schema. Formik + Yup:
const validationSchema = Yup.object({
  username: Yup.string()
    .min(3, 'At least 3 characters')
    .max(20, 'Max 20 characters')
    .matches(/^[a-z0-9_]+$/, 'Only lowercase, numbers, underscores')
    .required('Username is required'),
  age: Yup.number()
    .min(18, 'Must be 18 or older')
    .max(120)
    .required('Age is required'),
})
FormEngine built-in validators:
{
  "name": "username",
  "type": "Input",
  "props": { "label": "Username" },
  "validations": [
    { "type": "required", "message": "Username is required" },
    { "type": "minLength", "value": 3, "message": "At least 3 characters" },
    { "type": "maxLength", "value": 20, "message": "Max 20 characters" },
    { "type": "regex", "value": "^[a-z0-9_]+$", "message": "Only lowercase, numbers, underscores" }
  ]
},
{
  "name": "age",
  "type": "Input",
  "props": { "label": "Age", "type": "number" },
  "validations": [
    { "type": "required", "message": "Age is required" },
    { "type": "min", "value": 18, "message": "Must be 18 or older" },
    { "type": "max", "value": 120 }
  ]
}
For validators not covered by the built-ins, use Zod custom validators — see Validation with Zod.

Before/after: conditional fields

Formik with values check:
<Formik initialValues={{ accountType: '', companyName: '' }}>
  {({ values }) => (
    <Form>
      <Field as="select" name="accountType">
        <option value="personal">Personal</option>
        <option value="business">Business</option>
      </Field>

      {values.accountType === 'business' && (
        <Field name="companyName" placeholder="Company name" />
      )}
    </Form>
  )}
</Formik>
FormEngine renderWhen:
{
  "name": "accountType",
  "type": "Dropdown",
  "props": {
    "label": "Account type",
    "data": [
      { "label": "Personal", "value": "personal" },
      { "label": "Business", "value": "business" }
    ]
  }
},
{
  "name": "companyName",
  "type": "Input",
  "props": { "label": "Company name" },
  "renderWhen": "form.data.accountType === 'business'",
  "validations": [
    { "type": "required", "message": "Company name is required" }
  ]
}
FormEngine automatically skips validation on hidden fields. See Conditional fields tutorial.

Before/after: FieldArray (repeating sections)

Formik FieldArray:

<FieldArray name="items">
  {({ push, remove, form }) => (
    <div>
      {form.values.items.map((item: any, index: number) => (
        <div key={index}>
          <Field name={`items.${index}.name`} placeholder="Name" />
          <Field name={`items.${index}.qty`} type="number" />
          <button onClick={() => remove(index)}>Remove</button>
        </div>
      ))}
      <button onClick={() => push({ name: '', qty: 1 })}>Add item</button>
    </div>
  )}
</FieldArray>
FormEngine Repeater:
{
  "name": "items",
  "type": "Repeater",
  "props": {
    "label": "Items",
    "addButtonLabel": "Add item",
    "removeButtonLabel": "Remove"
  },
  "components": [
    {
      "name": "name",
      "type": "Input",
      "props": { "label": "Name" },
      "validations": [{ "type": "required" }]
    },
    {
      "name": "qty",
      "type": "Input",
      "props": { "label": "Quantity", "type": "number" }
    }
  ]
}
Submitted data: formData.items = [{ name: '...', qty: 1 }, ...].

Before/after: setFieldValue

Formik:
const { setFieldValue, values } = useFormik({ ... })

const handleCountryChange = (country: string) => {
  setFieldValue('country', country)
  setFieldValue('currency', country === 'US' ? 'USD' : 'EUR')
}
FormEngine setValues action:
{
  "name": "country",
  "type": "Dropdown",
  "props": { "label": "Country", "data": [...] },
  "actions": [
    {
      "event": "onChange",
      "action": "setValues",
      "args": {
        "currency": "{{form.data.country === 'US' ? 'USD' : 'EUR'}}"
      }
    }
  ]
}
See Actions and events reference.

Migrating initialValues

Formik uses initialValues to pre-populate the form. In FormEngine, set the defaultValue prop on individual components:
{
  "name": "country",
  "type": "Dropdown",
  "props": {
    "label": "Country",
    "defaultValue": "US",
    "data": [...]
  }
}
Or pre-populate the entire form by passing initialData to FormViewer:
<FormViewer
  view={view}
  form={schema}
  initialData={{ country: 'US', currency: 'USD' }}
/>

Migration strategy

Incremental (recommended):
  1. Install FormEngine: npm install @react-form-builder/core @react-form-builder/components-material-ui
  2. Pick one Formik form — ideally one with straightforward validation.
  3. Write the JSON schema for it.
  4. Replace the Formik component tree with <FormViewer>.
  5. Move validationSchema rules into the schema’s validations arrays.
  6. Move onSubmit to onActionEventAsync.
  7. Test, ship, then move to the next form.
Yup → built-in validators: Most Yup rules map directly. Use the built-in validator table as your reference. For complex Yup .test() rules, convert them to Zod custom validators.

What doesn’t migrate cleanly

Formik context consumers: If you have nested components that use useFormikContext() to read or write form state, those need to be rewritten. In FormEngine, form state is accessed via FormViewerRef from outside, or via form.data expressions inside the schema. Custom async validation with validate: Formik supports a validate async function per field. FormEngine supports async validators via validateAsync in the custom validator object — the concept is the same, the API is different. Formik’s isSubmitting and loading state: Formik tracks isSubmitting automatically. FormEngine doesn’t expose a built-in submitting state — manage it in your React component’s useState inside the onActionEventAsync handler.

Next steps

Last modified on April 16, 2026