Skip to main content

Introducing Workflow Engine, try for FREE workflowengine.io.

Formik integration

Introduction to FormEngine

In the world of React development, managing forms can often become a complex task. Enter Formik, a beloved open-source library that simplifies form management by handling state, validation, and submission seamlessly. However, if you're looking for even more features and capabilities, look no further than FormEngine. This powerful library not only matches Formik's capabilities but also extends them, offering advanced features for form management.

In this article, we’ll explore how to integrate Formik with FormEngine, allowing you to leverage the strengths of both libraries. Whether you want to reuse existing code or prefer the familiar Formik setup, this guide will help you get started.

Let's omit some boilerplate code and highlight only important things.

tip

Full source code along with other examples available at our public GitHub repository.

Code changes

Highlighted lines indicate the ones that need attention. If the file is created for the first time, then any modified lines are not marked in it. If the file is modified within the tutorial, then the modified, added, and deleted lines are marked in it as follows.

var x = 42; // highlighted line of code

var x = 42; // modified line of code

var x = 42; // added new line of code

var x = 42; // deleted line of code

Getting Started with FormEngine and Formik

To kick off, create a new React project and install the necessary dependencies. Open your terminal and run the following commands:

npx create-react-app with-formik --template typescript
cd with-formik
npm add formik yup @react-form-builder/core @react-form-builder/components-rsuite @react-form-builder/designer
npm run start

Initializing the FormEngine Designer

Next, replace the contents of App.tsx with the following code to set up FormEngine's Designer:

src/App.tsx
import React from 'react'
import {BuilderView, FormBuilder, IFormStorage} from '@react-form-builder/designer'
import {
formEngineRsuiteCssLoader,
ltrCssLoader,
RsLocalizationWrapper,
rSuiteComponents,
rtlCssLoader
} from '@react-form-builder/components-rsuite';
import {BiDi} from '@react-form-builder/core';

const componentsMetadata = rSuiteComponents.map(definer => definer.build())

const formName = 'formikForm'

const formStorage: IFormStorage = {
getForm: async () => localStorage.getItem(formName) || '{"form":{"key":"Screen","type":"Screen"}}',
saveForm: async (_, form) => localStorage.setItem(formName, form),
getFormNames: () => Promise.resolve([formName]),
removeForm: () => Promise.resolve()
}

const loadForm = () => formStorage.getForm('')

const builderView = new BuilderView(componentsMetadata)
.withViewerWrapper(RsLocalizationWrapper)
.withCssLoader(BiDi.LTR, ltrCssLoader)
.withCssLoader(BiDi.RTL, rtlCssLoader)
.withCssLoader('common', formEngineRsuiteCssLoader)

// We're hiding the form panel because it's not fully functional in this example
const customization = {
Forms_Tab: {
hidden: true
}
}

function App() {
return <FormBuilder view={builderView} formStorage={formStorage}
customization={customization} getForm={loadForm}/>
}

export default App

Now, you have a minimal FormEngine Designer up and running!

Designer

Creating a Simple Booking Form

Let’s create a simple booking form with the following fields:

  • Customer's Full Name
  • Check-in Date
  • Total Number of Guests

You can either drag and drop components in the designer or use a pre-made form definition bellow. Name fields accordingly: fullName, checkinDate, guestCount.

form.json
{
"version": "1",
"form": {
"key": "Screen",
"type": "Screen",
"props": {},
"children": [
{
"key": "fullName",
"type": "RsInput",
"props": {
"label": {
"value": "Full name"
}
}
},
{
"key": "checkinDate",
"type": "RsDatePicker",
"props": {
"label": {
"value": "Check-in date"
}
}
},
{
"key": "guestCount",
"type": "RsNumberFormat",
"props": {
"label": {
"value": "Guest count"
}
}
},
{
"key": "rsButton1",
"type": "RsButton",
"props": {
"children": {
"value": "Send"
}
}
}
]
},
"localization": {},
"languages": [
{
"code": "en",
"dialect": "US",
"name": "English",
"description": "American English",
"bidi": "ltr"
}
],
"defaultLanguage": "en-US"
}

Booking form

Integrating Formik with Your Booking Form

If you have an existing form implemented with Formik using hooks, you can easily integrate it with FormEngine. Here’s how you can set up the src/useBookingForm.ts:

src/useBookingForm.ts
import {useFormik} from 'formik';
import {useMemo, useState} from 'react';
import {FormikProps} from 'formik/dist/types';

export type BookingForm = Partial<{
fullName: string,
guestCount: number,
checkinDate: Date
}>

export type BookingFormErrors = Partial<Record<keyof BookingForm, string>>

export const useBookingForm = (): [FormikProps<BookingForm>, BookingForm] => {
const [formData, setFormData] = useState<BookingForm>({});

const initialValues = useMemo<BookingForm>(() => ({
fullName: '',
guestCount: 1,
checkinDate: new Date()
}), []);

const formik = useFormik<BookingForm>({
initialValues,
validate: ({checkinDate, fullName, guestCount}) => {
let errors: Partial<Record<keyof typeof initialValues, string>> = {};

if (!fullName) {
errors.fullName = 'Name is required.';
} else if (fullName.trim().split(' ').length < 2) {
errors.fullName = 'Please enter a full name.'
}

if (!checkinDate) {
errors.checkinDate = 'Date is required.';
}

if (!isFinite(guestCount as number) || (guestCount as number) < 1) {
errors.guestCount = 'No guest entered.';
}

return errors;
},
onSubmit: (data) => {
setFormData(data);
console.log(data);
formik.resetForm();
}
});

return [formik, formData]
}

Bridging FormEngine and Formik

Firstly, we need to change the target for TypeScript (because we are using iteration on object properties):

tsconfig.json
{
"compilerOptions": {
"target": "es2015",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}

To connect FormEngine with Formik, we need to pass values between them and handle validation errors. Update the App.tsx file, you can just replace the contents with the code below (major changes are highlighted):

src/App.tsx
import {ActionDefinition, BiDi, ComponentData, Field, IFormData} from '@react-form-builder/core';
import React, {useCallback, useMemo} from 'react'
import {BookingFormErrors, useBookingForm} from './useBookingForm'
import debounce from 'lodash/debounce';
import {BuilderView, FormBuilder, IFormStorage} from '@react-form-builder/designer'
import {
formEngineRsuiteCssLoader,
ltrCssLoader,
RsLocalizationWrapper,
rSuiteComponents,
rtlCssLoader
} from '@react-form-builder/components-rsuite';

const componentsMetadata = rSuiteComponents.map(definer => definer.build())

const formName = 'formikForm'

const formStorage: IFormStorage = {
getForm: async () => localStorage.getItem(formName) || '{"form":{"key":"Screen","type":"Screen"}}',
saveForm: async (_, form) => localStorage.setItem(formName, form),
getFormNames: () => Promise.resolve([formName]),
removeForm: () => Promise.resolve()
}

const loadForm = () => formStorage.getForm('')

const builderView = new BuilderView(componentsMetadata)
.withViewerWrapper(RsLocalizationWrapper)
.withCssLoader(BiDi.LTR, ltrCssLoader)
.withCssLoader(BiDi.RTL, rtlCssLoader)
.withCssLoader('common', formEngineRsuiteCssLoader)

// We're hiding the form panel because it's not fully functional in this example
const customization = {
Forms_Tab: {
hidden: true
}
}

function App() {
const [formik] = useBookingForm()

const setFormikValues = useMemo(() => debounce(async (form: IFormData) => {
const {fields} = form as ComponentData

for (const [key, {value}] of fields) {
const field = formik.getFieldProps(key)
if (value !== field.value) {
try {
await formik.setFieldValue(key, value)
await formik.validateField(key)
} catch (e) {
console.warn(e)
}
}
}
}, 400), [formik])

const setFormEngineErrors = useCallback((errors: BookingFormErrors, form: ComponentData) => {
const {fields} = form

Object.entries(errors).forEach(([key, error]) => {
if (fields.has(key)) {
(fields.get(key) as Field).setError(error)
}
})
}, [])

return <FormBuilder
view={builderView}
customization={customization}
formStorage={formStorage}
initialData={formik.values}
onFormDataChange={setFormikValues}
getForm={loadForm}
actions={{
submitForm: ActionDefinition.functionalAction(async (e) => {
const errors = await formik.validateForm()

if (Object.keys(errors).length > 0) {
return setFormEngineErrors(errors, e.store.formData)
}
await formik.submitForm()
}),
}}
/>
}

export default App

Notice that we've added a common action. Now you can bind the action to the Send button, as in the screenshot below.

Formik submit

Click to view the full JSON of the form.
{
"version": "1",
"form": {
"key": "Screen",
"type": "Screen",
"props": {},
"children": [
{
"key": "fullName",
"type": "RsInput",
"props": {
"label": {
"value": "Full name"
}
}
},
{
"key": "checkinDate",
"type": "RsDatePicker",
"props": {
"label": {
"value": "Check-in date"
}
}
},
{
"key": "guestCount",
"type": "RsNumberFormat",
"props": {
"label": {
"value": "Guest count"
}
}
},
{
"key": "rsButton1",
"type": "RsButton",
"props": {
"children": {
"value": "Send"
}
},
"events": {
"onClick": [
{
"name": "submitForm",
"type": "custom"
}
]
}
}
]
},
"localization": {},
"languages": [
{
"code": "en",
"dialect": "US",
"name": "English",
"description": "American English",
"bidi": "ltr"
}
],
"defaultLanguage": "en-US"
}

Now press Send button and see everything works, data passed to Formik and validation errors sent back to our form.

Form synced data and errors

Leveraging Yup for Validation

Yup is a powerful validation library that works seamlessly with Formik and is recommended by its team. To enhance our validation process, we can define Yup validators and integrate them into our FormEngine setup.

Let's rewrite our validation using Yup and use it. We are going to bind Yup validators directly to FormEngine native validation, but we also can use it inside Formik form definition as shown above.

Create a validators.ts file:

src/validators.ts
import * as Yup from 'yup'

export const fullName = Yup.string().required().test({
message: 'Please enter a full name',
test: (value: string) => !!value && value.trim().split(' ').length > 1
})

export const dateTodayOrInTheFuture = Yup.date().required().test({
message: 'Dates in the past are impossible to book',
test: (value: Date) => {
const today = new Date()
const date = new Date(value)

today.setHours(0, 0, 0, 0);
date.setHours(0, 0, 0, 0);

return date >= today
}
})

export const checkGuestsCount = Yup.number().required().min(1).max(6)

Define common validators and small helper function toFormEngineValidate which will be the bridge between two libraries. Please notice validation is now done by FormEngine and we sync errors back to Formik.

Then, update your App.tsx to utilize these validators within FormEngine (major changes are highlighted):

src/App.tsx
import {ActionDefinition, BiDi, ComponentData, IFormData, RuleValidatorResult, Validators} from '@react-form-builder/core';
import React, {useMemo} from 'react'
import {useBookingForm} from './useBookingForm'
import debounce from 'lodash/debounce';
import {BuilderView, FormBuilder, IFormStorage} from '@react-form-builder/designer'
import {
formEngineRsuiteCssLoader,
ltrCssLoader,
RsLocalizationWrapper,
rSuiteComponents,
rtlCssLoader
} from '@react-form-builder/components-rsuite';
import * as validator from './validators'
import {Schema} from 'yup'

const componentsMetadata = rSuiteComponents.map(definer => definer.build())

const formName = 'formikForm'

const formStorage: IFormStorage = {
getForm: async () => localStorage.getItem(formName) || '{"form":{"key":"Screen","type":"Screen"}}',
saveForm: async (_, form) => localStorage.setItem(formName, form),
getFormNames: () => Promise.resolve([formName]),
removeForm: () => Promise.resolve()
}

const loadForm = () => formStorage.getForm('')

const builderView = new BuilderView(componentsMetadata)
.withViewerWrapper(RsLocalizationWrapper)
.withCssLoader(BiDi.LTR, ltrCssLoader)
.withCssLoader(BiDi.RTL, rtlCssLoader)
.withCssLoader('common', formEngineRsuiteCssLoader)

// We're hiding the form panel because it's not fully functional in this example
const customization = {
Forms_Tab: {
hidden: true
}
}

const toFormEngineValidate = (yupValidator: typeof Schema.prototype) => async (value: unknown): Promise<RuleValidatorResult> => {
let err: RuleValidatorResult = true
try {
await yupValidator.validate(value)
} catch (e) {
err = (e as Error).message
}
return err
}

const customValidators: Validators = {
'string': {
'isFullName': {
validate: toFormEngineValidate(validator.fullName)
},
},
'date': {
'dateInTheFuture': {
validate: toFormEngineValidate(validator.dateTodayOrInTheFuture)
}
},
'number': {
'checkGuestCount': {
validate: toFormEngineValidate(validator.checkGuestsCount)
}
}
}

function App() {
const [formik] = useBookingForm()

const setFormikValues = useMemo(() => debounce(async (form: IFormData) => {
const {fields} = form as ComponentData

for (const [key, {value, error}] of fields) {
const field = formik.getFieldProps(key)
if (value !== field.value) {
try {
await formik.setFieldValue(key, value)
formik.setFieldError(key, error)
} catch (e) {
console.warn(e)
}
}
}
}, 400), [formik])

return <FormBuilder
validators={customValidators}
view={builderView}
customization={customization}
formStorage={formStorage}
initialData={formik.values}
onFormDataChange={setFormikValues}
getForm={loadForm}
actions={{
submitForm: ActionDefinition.functionalAction(async (e) => {
await e.store.formData.validate()

if (Object.keys(e.store.formData.errors).length < 1) {
await formik.submitForm()
}
}),
}}
/>
}

export default App

You can now add validators to fields on the form, full JSON form below.

Click to view the full JSON of the form.
{
"version": "1",
"form": {
"key": "Screen",
"type": "Screen",
"props": {},
"children": [
{
"key": "fullName",
"type": "RsInput",
"props": {
"label": {
"value": "Full name"
}
},
"schema": {
"validations": [
{
"key": "isFullName",
"type": "custom"
}
]
}
},
{
"key": "checkinDate",
"type": "RsDatePicker",
"props": {
"label": {
"value": "Check-in date"
}
},
"schema": {
"validations": [
{
"key": "dateInTheFuture",
"type": "custom"
}
]
}
},
{
"key": "guestCount",
"type": "RsNumberFormat",
"props": {
"label": {
"value": "Guest count"
}
},
"schema": {
"validations": [
{
"key": "checkGuestCount",
"type": "custom"
}
]
}
},
{
"key": "rsButton1",
"type": "RsButton",
"props": {
"children": {
"value": "Send"
}
},
"events": {
"onClick": [
{
"name": "submitForm",
"type": "custom"
}
]
}
}
]
},
"localization": {},
"languages": [
{
"code": "en",
"dialect": "US",
"name": "English",
"description": "American English",
"bidi": "ltr"
}
],
"defaultLanguage": "en-US"
}

FormEngine validators

No need for embedded inline form validators, so drop them.

useBookingForm.ts
// ...

export const useBookingForm = (): [FormikProps<BookingForm>, BookingForm] => {
// ...
const formik = useFormik<BookingForm>({
initialValues,
validate: ({checkinDate, fullName, guestCount}) => {
let errors: Partial<Record<keyof typeof initialValues, string>> = {};

if (!fullName) {
errors.fullName = 'Name is required.';
} else if (fullName.trim().split(' ').length < 2) {
errors.fullName = 'Please enter a full name.'
}

if (!checkinDate) {
errors.checkinDate = 'Date is required.';
}

if (!isFinite(guestCount as number) || (guestCount as number) < 1) {
errors.guestCount = 'No guest entered.';
}

return errors;
},
onSubmit: (data) => {

If you fill out the form, you will see that it is now processed by separate Yup validators.

Yup validators

Conclusion

By integrating FormEngine with Formik, you can create powerful, user-friendly forms in your React applications. This combination allows you to leverage the strengths of both libraries, ensuring efficient form management and validation. With the steps outlined in this guide, you can easily sync data and validation errors between FormEngine and Formik, enhancing your development experience.

For the complete source code and additional examples, visit our public GitHub repository.

Start building better forms today with FormEngine and Formik!