Build forms in React, without the tears
Formik helps you with :
- input validation
- formatting
- error handling
We'll discuss the following subjects :
- Validation Schema
- Formik Wrapper
- useFormikContext Hook
- How to generate Validation Schema and Initial Values dynamically.
- Show Errors on touch inside your custom field.
- Show Internalization Error Messages depending on your schema.
- How I used Formik for my project needs.
Formik wraps your form inside a context, inside the multiple context that it has, it will validate your schema, update your fields and keep your state.
Custom OnSubmit React Hook
I've created a custom hook, here you can add your event handlers in case you have multiple forms on your website, keeping the onSubmit logic separated by the UI Component.
export default function useFormHandlers(){
const contactOnSubmit = useCallback(async (values) => {
// do something here
// await (POST)
},[])
return {
contactOnSubmit
}
}
Formik Wrapper UI aka the Parent
We imported useFormHandlers, we'll use it inside Formik OnSubmit function to send the values. In case you want to create custom styling inside the wrapper, you can use isSubmitting.
const eventHandler = useFormHandlers();
return (
<Formik
initialValues={initialValues}
schemaValidation={schemaValidation}
onSubmit={async (values, { setSubmitting }) => {
await eventHandler.contactOnSubmit.bind(this,values);
}}
>
{({
isSubmitting , handleSubmit,
}) => (
<form onSubmit={handleSubmit} className="bg-white">
{/* Fields */}
{/* Custom Fields */}
</form>
)}
</Formik>
)
Formik Fields UI aka the Child
Formik does an excelent job at keeping your state according to the input name field. The "name" is the principal tag of keeping the state and obtaining the context.
Formik Field
<Field type="text" name="username" placeholder="Username" />
Custom Formik Field
First, we'll want to create a new component, I have provided below how to set the field value of the formik value. You can see the whole list from "useFormikContext" Formik Docs. There are multiple hooks there from submitCount, errors object, fields props and metas and so on.
import { useField, useFormikContext } from "formik";
import { useCallback } from "react";
const CustomInput = ({ name, ...props }) => {
const [field] = useField(name);
// Get the full Formik Context using useFormikContext
const formikCtx = useFormikContext();
const eventHandler = useCallback((value) => {
formikCtx.setFieldValue(name,value)
},[])
return (
<>
<input
type="text"
name={name}
value={field.value}
onChange={eventHandler.bind(this)}
disabled={formikCtx.isSubmitting}
/>
{/* In case we have schema validation, we can provide the error message. This works similar for the Formik Fields. */}
{
formikCtx.errors[name] && formikCtx.touched[name] &&
<p>{formikCtx.errors[name]}</p>
}
</>
);
};
export default CustomInput;
Import the custom field input from your parent and voila! Now you have a custom input inside the formik.
<CustomInput name="username" />
Schema Validation with Internalization via Yup
For my project, I had an object obtained from CMS with the following structure :
CMSFields :
[
{
"id" : 1,
"label" : "Username",
"required" : true,
"minCharacters" : 4,
"maxCharacters" : 16,
"type" : "text",
"defaultValue" : null,
"name" : "username",
"placeholder" : "formikrocks"
},
{
"id" : 2,
"label" : "Email Address",
"required" : true,
"minCharacters" : 6,
"maxCharacters" : 32,
"type" : "email",
"defaultValue" : null,
"name" : "email",
"placeholder" : "formikrocks@formik.com"
}
]
With the following translations:
{
"username" : {
"en" : "This username is required.",
"fr" : "Ce nom d'utilisateur est requis."
},
"minimumFieldLength" : {
"en" : "Must be __VALUE__ characters or more",
},
"maximumFieldLength" : {
"en" : "Must be __VALUE__ characters or less",
},
"fieldMatch" : {
"en" : "__VALUE__ must match",
},
"default" : {
"en" : "This field is required.",
"fr" : "Ce champ est required."
}
}
Now the next thing is to parse the CMSfield JSON using the following function :
import * as Yup from "yup";
import translationErrors from "../translationErrors";
export default function generateSchemaValidation(fields, locale) {
const schema = {};
const getErrorMessage = (nameRule,replaceValue = null) => {
let message = '';
try {
message = translationErrors[nameRule][locale]
if(replaceValue) message = message.replace('__VALUE__',replaceValue);
} catch(err) {
message = translationErrors?.[default]?.[locale]
}
return message;
}
fields?.forEach((field) => {
let name = field.name;
if (field.type === "text") schema[name] = Yup.string().nullable();
if (field.type === "checkbox") schema[name] = Yup.boolean().nullable();
if (field.type === "number") schema[name] = Yup.number().nullable();
if (field.type === "date") schema[name] = Yup.date().nullable();
if (schema[name] === undefined) schema[name] = Yup.string().nullable();
if (field.required) schema[name] = schema[name].required(getErrorMessage(name));
if (field.minCharacters) schema[name] = schema[name].min(field?.minCharacters, getErrorMessage('minimumFieldLength',field.minCharacters));
if (field.maxCharacters) schema[field.name] = schema[field.name].max(field?.maxCharacters,getErrorMessage('maximumFieldLength',field.maxCharacters));
if (field.type === "checkbox" && field.required) schema[name] = schema[name].oneOf([true],getErrorMessage(name));
// if we have confirm_, we can have the following case (works for email or password) - the "ref" requires another field's name value
if (name && name?.indexOf("confirm_") >= 0) {
if (field.type === "email") {
schema[name] = schema[name].oneOf(
[Yup.ref("email"), null],
getErrorMessage('fieldMatch',name)
);
}
if (field.type === "password") {
schema[name] = schema[name].oneOf(
[Yup.ref("password"), null],
getErrorMessage('fieldMatch',name)
);
}
}
});
return Yup.object().shape(schema);
}
In case you have the same structure, you can use the following parsing function to generate initial values in the following structure :
{
"username" : null,
"email" : null
}
Generate Initial Values
function getObjectIntialValue(name, value) {
if (value === false) return { [name]: false };
return {
[name]: value || null,
};
}
export default function generateInitialValues(fields) {
const initialValues = fields?.map((el) => {
if (el.type === "text") return getObjectIntialValue(el.name, el.defaultValue);
if (el.type === "checkbox") return getObjectIntialValue(el.name, el.defaultValue);
return getObjectIntialValue(el.name);
});
const initialValuesParsed = initialValues.reduce((obj, item) => {
const [key, value] = Object.entries(item)[0];
obj[key] = value;
return obj;
}, {});
return initialValuesParsed;
}
The use case I needed was to parse the values in order to build a dynamic form using the CMS data to generate initialValues and validationSchema for the FormikWrapper. Moreover, you can add rules to your fields, and match them inside the validationSchema in case you need a proper structure using regex.
Thank you for reading,
Mihai
Top comments (0)