Form Development in React comes down to three things: Data, Validations, and Submission. See how to handle these yourself or using Formik to make things simpler.
As your form in React becomes more complicated, you will find yourself reinventing more and more of the functionality that comes with Formik. If you find manually controlling a form and its validation painful, it may be time to switch to Formik or another form package to help make this process a bit easier to manage. In this article we'll investigate forms in Vanilla React and compare that to forms with Formik.
When you’re thinking of forms in React, there are three things to keep in mind:
- How do I access what the user entered?
- How do I ensure what they entered is valid?
- How do I submit their data to the server?
That order is important because you can’t do step two unless you have first done step one, and you wouldn’t want to submit invalid data to the server.
This article will show the basics of Forms in React, both with and without the help of additional packages. We’ll see how to do “Vanilla React Forms”, and then how to accomplish the same thing using the Formik package from Jared Palmer.
My thesis is that the simpler the form, the more you can lean on React without additional packages, but as the number of fields increases and the validations get trickier, we’ll tend to stick with Formik to avoid rebuilding Formik ourselves!
All examples in their entirety can be found here on GitHub.
Vanilla React Forms
When I say “Vanilla React Forms,” I am referring to nothing else other than React… no additional packages. As you’ll see in this section, it could start to get out of control pretty quickly, as with just a single input that has some validations, it’s already turning into a decent-sized component.
Controlled Components
To answer “How do I access what the user entered?” we will use Controlled Components. Controlled Components are where the user’s input will trigger an update to the component’s state, which will cause a re-render of the component, displaying what the user entered.
By using the onChange
event on an input
field, we can update the state. Then, having the value
prop equal to the value in our state, we can display it to the user.
exportdefaultfunctionControlled(){const[value, setValue]= React.useState("");return(<form><inputtype="text"placeholder="Controlled Name"onChange={event =>setValue(event.target.value)}value={value}/></form>);}
Validating Data
To validate our user’s input, we’ll maintain an object of errors
in our state. This will get populated any time the user changes a value in the form and prior to the form’s submission. Leaving aside form submission for now, let’s look at the validate
function. It will start fresh every time, populating an errors object based on the current values in our form.
functionvalidate(values){let errors ={};if(!values.name){
errors.name ="Required";}return errors;}
Using the useEffect
hook, we can detect when any of the input values change, calling the validate
function and placing its result into our state. With an errors
object, we can optionally add a class to our input field by looking to see if the field has an error: className={errors.name ? "has-error" : null}
. Below the input field, we pass the error message to a component called Error
which will render the message (if it exists) into an element with the correct classes.
exportdefaultfunctionVanillaForm(){const[submitting, setSubmitting]= React.useState(false);const[name, setName]= React.useState("");const[errors, setErrors]= React.useState({});// Recalculate errors when any of the values change
React.useEffect(()=>{setErrors(validate({ name }));},[name]);return(<formonSubmit={event =>{
event.preventDefault();}}><h2>An Average Form</h2><divclassName="input-row"><label>Name</label><inputtype="text"name="name"onChange={event =>{setName(event.target.value);}}value={name}className={errors.name ?"has-error":null}/><Errormessage={errors.name}/></div><divclassName="input-row"><buttontype="submit"disabled={submitting}>
Submit
</button></div></form>);}
Submitting Data
Finally, with our input value inside of name
and the validation handled, it’s time to submit the form. A normal HTML form uses the form’s action
prop, containing a URL to POST the data to, but in this case we will use the form’s onSubmit
event to take matters into our own hands.
In order to stop the form from submitting via the normal method, we’ll call event.preventDefault()
. Just to ensure our validation is completely up to date, we can call the validate
check one last time. After that, it’s just a matter of posting the data somewhere using fetch, Axios, or perhaps with a mutation in GraphQL. In this case we’ll alert the data so we can see it in the browser.
event =>{// Stop the form from submitting
event.preventDefault();// Validate the data one last timeif(Object.keys(validate({ name })).length >0){return;}// Update the submitting state to truesetSubmitting(true);// Time to process the datasetTimeout(()=>{const values ={ name };alert(JSON.stringify(values,null,2));setSubmitting(false);},500);};
Formik
For more complicated forms - perhaps with multiple fields or validations - it’s time to reach for a package called Formik. The principles are the same as we covered above, but it handles a lot of the heavy lifting for us. In this form, we’ll consider some more advanced use cases, including conditionally displaying fields and validating them, based on a value from an Autosuggest field.
In order to focus on the functionality we are discussing, I am going to slice and dice this somewhat large component to show what is important to the specific example. You can find the entire component here.
Accessing Data
Formik provides us with a values
object. It gets its initial values using the initialValues
prop, and then is updated automatically by the onChange
event on each individual field. An important thing to keep in mind is that Formik uses the name
prop of each input to know which value to set.
exportdefaultfunctionFormikForm(){return(<FormikinitialValues={{
name:"",
email:"",
country:"",
postalCode:""}}>{({
values,
errors,
handleChange,
handleBlur,
handleSubmit,
isSubmitting,
setFieldValue
})=>(<formonSubmit={handleSubmit}><h2>A Great Form</h2><divclassName="input-row"><label>Name</label><inputtype="text"name="name"onChange={handleChange}onBlur={handleBlur}value={values.name}className={errors.name ?"has-error":null}/><Errormessage={errors.name}/></div>{/* Additional fields here */}<divclassName="input-row"><buttontype="submit"disabled={isSubmitting}>
Submit
</button></div></form>)}</Formik>);}
Validating Data
Formik provides two main ways to validate user data: The first approach requires us to populate an errors
object, similar to how it was done in the Vanilla React examples. The second approach uses Yup to define a validation schema, handling validation in a structured and simple way.
const ValidationSchema = Yup.object().shape({
name: Yup.string().min(1,"Too Short!").max(255,"Too Long!").required("Required"),
country: Yup.string().min(1,"Too Short!").max(255,"Too Long!").required("Required"),
email: Yup.string().email("Must be an email address").max(255,"Too Long!").required("Required")});
With our validation schema in place, we can pass it to the Formik component. At the same time, we’ll pass a function to the validate
prop so we can add errors ourselves when Yup doesn’t cut it. This will be explained in further detail when we discuss conditional fields.
<Formik
validationSchema={ValidationSchema}
validate={values =>{let errors ={};// Validate the Postal Code conditionally based on the chosen Countryif(!isValidPostalCode(values.postalCode, values.country)){
errors.postalCode =`${postalCodeLabel(values.country)} invalid`;}return errors;}}>{/* Fields here... */}</Formik>
Errors are then accessed with the errors
object passed via the render prop function. You can see how they are used to add a class to the input and display errors below:
<divclassName="input-row"><label>Name</label><inputtype="text"name="name"onChange={handleChange}onBlur={handleBlur}value={values.name}className={errors.name ?"has-error":null}/><Errormessage={errors.name}/></div>
Autosuggest with Formik
A common use case when building a form is to have an autosuggest/autocomplete field, where, as you type, the suggested values are displayed below for the user to select. For this we’ll use react-autosuggest. The field will allow the user to search from a list of countries (retrieved from a JSON feed).
In this case we won’t update our Formik country
value as the user types each character, but instead set it ourselves using the setFieldValue
function. This means that Formik is only aware of the country value when the user selects a suggestion. The react-autosuggest package requires us to control the input values, so we’ll declare country
and suggestions
state values.
Before looking at the entire example, we’ll see what happens when a user makes a selection. Using the onSuggestionSelected
prop, we can call setFieldValue
:
(event,{ suggestion, method })=>{// Stop form from submitting by preventing default actionif(method ==="enter"){
event.preventDefault();}// Update country state, this is used by us and react-autosuggestsetCountry(suggestion.name);// Update country value in FormiksetFieldValue("country", suggestion.name);};
Note that when the “method” (how the suggestion was selected) equals “enter,” we’ll prevent default for this event, because otherwise the form will be submitted, when the user just wanted to select a suggestion.
Below we have the full example, which may seem rather long, but there are a number of props that control how the suggestions are fetched and then rendered. Notice that I still use errors
provided by Formik. Because of our use of setFieldValue
, Formik will view it as invalid until the user selects a suggestion from the list.
exportdefaultfunctionFormikForm(){const[country, setCountry]= React.useState("");const[suggestions, setSuggestions]= React.useState([]);return(<Formik>{({
values,
errors,
handleChange,
handleBlur,
handleSubmit,
isSubmitting,
setFieldValue
})=>(<formonSubmit={handleSubmit}><divclassName="input-row"><label>Country</label><Autosuggest
suggestions={suggestions}
onSuggestionsFetchRequested={async({ value })=>{// An empty value gets no suggestionsif(!value){setSuggestions([]);return;}// Try to populate suggestions from a JSON endpointtry{const response =await axios.get(`https://restcountries.eu/rest/v2/name/${value}`);setSuggestions(
response.data.map(row =>({
name: row.name,
flag: row.flag
})));}catch(e){setSuggestions([]);}}}
onSuggestionsClearRequested={()=>{setSuggestions([]);}}
getSuggestionValue={suggestion => suggestion.name}
renderSuggestion={suggestion =><div>{suggestion.name}</div>}
onSuggestionSelected={(event,{ suggestion, method })=>{if(method ==="enter"){
event.preventDefault();}setCountry(suggestion.name);setFieldValue("country", suggestion.name);}}
inputProps={{
placeholder:"Search for your country",
autoComplete:"abcd",
value: country,
name:"country",
onChange:(_event,{ newValue })=>{setCountry(newValue);},
className: errors.country ?"has-error":null}}/><Errormessage={errors.country}/></div></form>)}</Formik>);}
Conditional Fields
Now that the user has chosen their country from the autosuggest list, we will optionally display a Postal Code field. Due to “budgetary restrictions,” our boss only wants to show this field to users from USA and Canada. Because the US uses ZIP Code, and Canada uses Postal Code, each with their own set of validation rules, we’ll be using the country value to determine which label to display and which validation rule to use.
I have found Yup perfect for straightforward “fixed” validations, but in this case it made sense to handle validations ourselves in Formik:
functionisValidPostalCode(postalCode, country){let postalCodeRegex;switch(country){case"United States of America":
postalCodeRegex =/^([0-9]{5})(?:[-\s]*([0-9]{4}))?$/;break;case"Canada":
postalCodeRegex =/^([A-Z][0-9][A-Z])\s*([0-9][A-Z][0-9])$/;break;default:returntrue;}return postalCodeRegex.test(postalCode);}functionpostalCodeLabel(country){const postalCodeLabels ={"United States of America":"Zip Code",
Canada:"Postal Code"};return postalCodeLabels[country]||"Postal Code";}functionshowPostalCode(country){return["United States of America","Canada"].includes(country);}exportdefaultfunctionFormikForm(){return(<Formik
validationSchema={ValidationSchema}
validate={values =>{let errors ={};// Validate the Postal Code conditionally based on the chosen Countryif(!isValidPostalCode(values.postalCode, values.country)){
errors.postalCode =`${postalCodeLabel(values.country)} invalid`;}return errors;}}>{({
values,
errors,
handleChange,
handleBlur,
handleSubmit,
isSubmitting,
setFieldValue
})=>(<formonSubmit={handleSubmit}>{showPostalCode(values.country)?(<divclassName="input-row"><label>{postalCodeLabel(values.country)}</label><inputtype="text"name="postalCode"onChange={handleChange}onBlur={handleBlur}value={values.postalCode}className={errors.postalCode ?"has-error":null}/><Errormessage={errors.postalCode}/></div>):null}</form>)}</Formik>);}
Submitting Data
Formik provides us with an onSubmit
prop to handle form submission. We don’t have to “prevent default” like we did when managing this directly ourselves, and instead we are provided with all of the form’s values, along with a function called setSubmitting
to control a Boolean value of whether or not the form is being submitted, and resetForm
to set the form back to its initial state.
(values,{ setSubmitting, resetForm })=>{setSubmitting(true);setTimeout(()=>{alert(JSON.stringify(values,null,2));resetForm();setCountry("");setSubmitting(false);},500);};
Conclusion
Forms in React — when you strip everything else away — involve the onSubmit
event on the form element and the onChange
event on each individual input. As your form becomes more complicated, you will find yourself reinventing more and more of the functionality that comes with Formik. If you find manually controlling a form and its validation painful, it may be time to switch to Formik or another form package to help make this process a bit easier to manage.
Keep Reading
Keep learning about Formik with this next post, Build Better React Forms with Formik.