- Sotiris Karapostolou
Managing Forms in React-Native. Build your own Formik with Validation using Hooks, Context API & Yup

As a frontend developer I've used and implemented many libraries, but one of my favorite was Formik that's why I've decided to build it myself and I must say it was very plesant.
I was working on the Setting page the other day, As you see in the gif above it's just a screen where the user can view or update his info.
We’re Building
There are couple of things happening in this form:
The form holds initial values that can be changed and get submited with an onSubmit() callback function
It validates user input with a provided validationShema using Yup
Throws erros based on the validation shema
Sets focus on the next input by using a registeredField() function behind the scenes that wires up refs
Reduces a lot of boilerplate and gives us easy access to form's state (values, errors, touched, focused)
Provides us a custom textInput, submit button and label components with our own defined styles for our accross platform unique styling that handles all the functionality behind the scens and updates the UI whenever a user interacts with it
And many more. The most fun part is that we just use React and pure functions.
Once we set it up it's just a few lines of code that gets the job done!!
Now let's look have a look at the Settings.js component:
#Settings.js
import * as Yup from 'yup';
import { CustomFormik, FormButton, Field } from 'services/CustomFormik';
const UserSettings = ({ navigation }) => {
const profileData = navigation.getParam('profileData');
const dispatch = useDispatch();
const initialValues = {
name: profileData.name ?? '',
email: profileData.email ?? '',
....
};
const validationShema = Yup.object().shape({
name: Yup.string().required('Required'),
email: Yup.string().email('"Invalid email address'),
....
});
const settingInputs = Object.keys(initialValues);
return (
<CustomFormik
initialValues={initialValues}
validationShema={validationShema}
onSumbit={({ values }) => {
dispatch(updateUserProfileStart(values));
}}
>
<ScrollView>
{settingInputs.map((input, i) => (
<Field
name={input}
key={i}
label={input}
containerStyles={{ marginVertical: 10 }}
placeholderTextColor={Colors.INPUT_TEXT}
focusNext={settingInputs[i + 1]}
/>
))}
<FormButton
loading={loading}
title="Login"
color={Colors.SECONDARY}
textColor={Colors.WHITE}
/>
</ScrollView>
</CustomFormik>
);
};
Easy right ??
Setup
To get started I’ve put together a repository on github to view the full code
First let's install this package so we can use it later
$ npm install yup
The basic structure of the CustomFormik.js file looks like:
#CustomFormik.js
import React, { useState, useRef, createContext } from 'react';
const CustomForm = createContext({});
export const CustomFormik = (props) => {
const [formData, setFormData] = useState({...});
const fieldRegistry = useRef({});
...
return (
<CustomForm.Provider value={...}>
...
</CustomForm.Provider>
);
};
export const Field = (props) => (
<CustomForm.Consumer>
{(props) => {
return (
...
);
}}
</CustomForm.Consumer>
);
export const FormButton = (props) => (
<CustomForm.Consumer>
{(props) => (
....
)}
</CustomForm.Consumer>
);
So far our form doesn't do anything, we just created the context so our components can share values between each other
Next we will take a closer look to each component. First, we start with CustomFormik
export const CustomFormik = (props) => {
const [formData, setFormData] = useState({
values: props.initialValues || {},
touched: {},
error: {},
focused: undefined,
isSubmiting: false,
});
const fieldRegistry = useRef({});
// This way we keep track of the input's refs by registering them to the fieldRegistry.
const registeredField = (name) => (ref) => {
if (fieldRegistry.current) {
fieldRegistry.current[name] = { ref };
}
};
// This is the input change handler, it takes user's input and updates the state with the new value.
const handleChange = (name) => (text) => {
setFormData((prev) => ({
...prev,
values: {
...prev.values,
[name]: text,
},
}));
};
// This is the onBlur event handler, when the user removes focus from an input, this function is called. Without it, if there are any errors in the input when it loses focus, the errors will only display when the user tries to submit.
const handleBlur = (name) => (text) => {
setFormData((prev) => ({
...prev,
touched: {
...prev.touched,
[name]: true,
},
}));
};
// This is the onFocus event handler, that watches if a form field has focus,it updates the state and sets focus to the field.
const setFocus = (name) => () => {
if (fieldRegistry.current) {
setFormData((prev) => ({
...prev,
focused: name,
}));
if (name) fieldRegistry.current[name].ref.focus();
}
};
// Keep tracks of the validation errors.
const setErros = (name, message) => {
setFormData((prev) => ({
...prev,
error: {
...prev.error,
[name]: message,
},
}));
};
// Validates a deeply nested path within the schema.
async function onValidateAt(name) {
try {
await props.validationShema.validateAt(name, formData.values).then(() => {
setFormData((prev) => ({
...prev,
error: {
...prev.error,
[name]: undefined,
},
}));
});
} catch (err) {
setErros(err.path, err.message);
}
}
// Validates all the values and updates the error Obj if needed.
async function onValidate() {
try {
await props.validationShema
.validate(formData.values, { abortEarly: false })
.then(() => {
setFormData((prev) => ({
...prev,
error: {},
}));
});
} catch (err) {
err.inner.forEach((e) => {
setErros(e.path, e.message);
});
}
}
// The form submission handler checks the validationShema.isValid for errors and then passes important information to the provided callback.
async function handleSubmit() {
if (props.validationShema) {
onValidate();
await props.validationShema
.isValid(formData.values)
.then(function (valid) {
if (valid) props.onSumbit({ ...formData });
});
} else {
props.onSumbit({ ...formData });
}
}
// In case it's children needs access to the state.
function checkChildren() {
if (typeof props.children === 'object') {
return props.children;
} else {
return props.children({ ...formData });
}
}
// We update the value object of the provider component to allow consuming components to subscribe to context changes.
return (
<FormikData.Provider
value={{
...formData,
handleChange,
handleBlur,
handleSubmit,
onValidateAt,
registeredField,
setFocus,
}}
>
{checkChildren()}
</FormikData.Provider>
);
}; // End of CustomFormik component
We covered a lot of functionality needed to get our form working but once you set it up it's a matter of few line (see Settings.js at the top of the article) to build a new form whenever your application needs it.
Next let's make a few changes to the Field component to listen to the functions we've just implemented.
export const Field = ({
name,containerStyles,focusNext,label, ...otherProps
}) => (
<FormikData.Consumer>
{({
handleChange,handleBlur,onValidateAt,registeredField,values,
error,setFocus,focused }) => {
return (
<View style={containerStyles}>
{label && (
<Text>{label}</Text>
)}
<TextInput
value={values[name]}
ref={registeredField(name)}
onSubmitEditing={setFocus(focusNext)}
onChangeText={handleChange(name)}
onBlur={handleBlur(name)}
onFocus={setFocus(name)}
onEndEditing={onValidateAt.bind(this, name)}
returnKeyType={focusNext ? 'next' : 'done'}
style={{
borderColor: focused === name ? '#anyColor' : '#anyColor',
}}
{...otherProps}
/>
{error[name] && (
<Text style={{ color: Colors.ERROR}}>
{error[name]}
</Text>
)}
</View>
);}}
</FormikData.Consumer>
);
Finally to be able to submit the form let's tweak the FormButton component
export const FormButton = (props) => {
return (
<FormikData.Consumer>
{({ handleSubmit }) => (
<>
{props.loading ? (
<ActivityIndicator color={Colors.PRIMARY} />
) : (
<Button
title={props.title}
color={props.color}
onPress={handleSubmit}
/>
)}
</>
)}
</FormikData.Consumer>
);
};
It goes without saying that you can use any button you prefer, In my case for the UserSettings component I am using a React Native Floating Action button that I imported from this library:
$ npm i react-native-floating-action
To Sum Up
Building your own components is such a great exercise to just sit down, spend the afternoon, build it out, tweak it, try to optimize that, and all that stuff. It's a fun, great way to play with it, I think.
Well, I hope that helped! Right to the point! If you like this and want to support me on making more stories you can contact me on Linkedin
#react#reactNative#hooks#contextAPI#yup#formik#forms#buildItYourSelf