+ REACT
RESOURCES
- Handling Submit
- https://dev.to/brad_beggs/handling-react-form-submit-with-redirect-async-await-for-the-beyond-beginner-57of
- EntryTwo
- EntryThree
-
- Forms Overall
- FORMIK: https://codesandbox.io/s/zKrK5YLDZ
- SubListEntryTwo
- SubListEntryThree
- SubListEntryFour
https://www.digitalocean.com/community/tutorials/how-to-build-forms-in-react
How To Build Forms in React
The author selected Creative Commons to receive a donation as part of the Write for DOnations program.
Introduction
Forms are a crucial component of React web applications. They allow users to directly input and submit data in components ranging from a login screen to a checkout page. Since most React applications are single page applications (SPAs), or web applications that load a single page through which new data is displayed dynamically, you won’t submit the information directly from the form to a server. Instead, you’ll capture the form information on the client-side and send or display it using additional JavaScript code.
React forms present a unique challenge because you can either allow the browser to handle most of the form elements and collect data through React change events, or you can use React to fully control the element by setting and updating the input value directly. The first approach is called an uncontrolled component because React is not setting the value. The second approach is called a controlled component because React is actively updating the input.
In this tutorial, you’ll build forms using React and handle form submissions with an example app that submits requests to buy apples. You’ll also learn the advantages and disadvantages of controlled and uncontrolled components. Finally, you’ll dynamically set form properties to enable and disable fields depending on the form state. By the end of this tutorial, you’ll be able to make a variety of forms using text inputs, checkboxes, select lists, and more.
Prerequisites
- You will need a development environment running Node.js; this tutorial was tested on Node.js version 10.20.1 and npm version 6.14.4. To install this on macOS or Ubuntu 18.04, follow the steps in How to Install Node.js and Create a Local Development Environment on macOS or the Installing Using a PPA section of How To Install Node.js on Ubuntu 18.04.
- A React development environment set up with Create React App, with the non-essential boilerplate removed. To set this up, follow Step 1 — Creating an Empty Project of the How To Manage State on React Class Components tutorial. This tutorial will use
form-tutorial
as the project name. - You will be using React events and Hooks, including the
useState
and theuseReducer
Hooks. You can learn about events in our How To Handle DOM and Window Events with React tutorial, and Hooks at How to Manage State with Hooks on React Components. - You will also need a basic knowledge of JavaScript and HTML, which you can find in our How To Build a Website with HTML series and in How To Code in JavaScript. Basic knowledge of CSS would also be useful, which you can find at the Mozilla Developer Network.
Step 1 — Creating a Basic Form with JSX
In this step, you’ll create an empty form with a single element and a submit button using JSX. You’ll handle the form submit event and pass the data to another service. By the end of this step, you’ll have a basic form that will submit data to an asynchronous function.
To begin, open App.js
:
nano src/components/App/App.js
You are going to build a form for purchasing apples. Create a [`](https://www.digitalocean.com/community/tutorial_series/how-to-build-a-website-with-html#how-to-use-a-,-the-html-content-division-element) with a
classNameof
. Then add an
h3tag with the text “How About Them Apples” and an empty
form` element by adding the following highlighted code:
form-tutorial/src/components/App/App.js
import React from 'react';
import './App.css';
function App() {
return (
<div className="wrapper">
<h3>How About Them Apples</h3>
<form>
</form>
</div>
)
}
export default App;
Next, inside the <form>
tag, add a <fieldset>
element with an <input>
element surrounded by a <label>
tag. By wrapping the <input>
element with a <label>
tag, you are aiding screen readers by associating the label with the input. This will increase the accessibility of your application.
Finally, add a submit <button>
at the bottom of the form:
form-tutorial/src/components/App/App.js
import React from 'react';
import './App.css';
function App() {
return(
<div className="wrapper">
<h3>How About Them Apples</h3>
<form>
<fieldset>
<label>
<p>Name</p>
<input name="name" />
</label>
</fieldset>
<button type="submit">Submit</button>
</form>
</div>
)
}
export default App;
Save and close the file. Then open App.css
to set the styling:
nano src/components/App/App.css
Add padding
to the .wrapper
and margin
to the fieldset
to give some space between elements:
form-tutorial/src/components/App/App.css
.wrapper {
padding: 5px 20px;
}
.wrapper fieldset {
margin: 20px 0;
}
Save and close the file. When you do, the browser will reload and you’ll see a basic form.
If you click on the Submit button, the page will reload. Since you are building a single page application, you will prevent this standard behavior for a button with a type="submit"
. Instead, you’ll handle the submit
event inside the component.
Open App.js
:
nano src/components/App/App.js
To handle the event, you’ll add an event handler to the <form>
element, not the <button>
. Create a function called handleSubmit
that will take the SyntheticEvent
as an argument. TheSyntheticEvent
is a wrapper around the standard Event
object and contains the same interface. Call .preventDefault
to stop the page from submitting the form then trigger an alert
to show that the form was submitted:
form-tutorial/src/components/App/App.js
import React from 'react';
import './App.css';
function App() {
const handleSubmit = event => {
event.preventDefault();
alert('You have submitted the form.')
}
return(
<div className="wrapper">
<h3>How About Them Apples</h3>
<form onSubmit={handleSubmit}>
<fieldset>
<label>
<p>Name</p>
<input name="name" />
</label>
</fieldset>
<button type="submit">Submit</button>
</form>
</div>
)
}
export default App;
Save the file. When you do the browser will reload. If you click the submit button, the alert will pop up, but the window will not reload.
In many React applications, you’ll send the data to an external service, like a Web API. When the service resolves, you’ll often show a success message, redirect the user, or do both.
To simulate an API, add a setTimeout
function in the handleSubmit
function. This will create an asynchronous operation that waits a certain amount of time before completing, which behaves similarly to a request for external data. Then use the useState
Hook to create a submitting
variable and a setSubmitting
function. Call setSubmitting(true)
when the data is submitted and call setSubmitting(false)
when the timeout is resolved:
form-tutorial/src/components/App/App.js
import React, { useState } from 'react';
import './App.css';
function App() {
const [submitting, setSubmitting] = useState(false);
const handleSubmit = event => {
event.preventDefault();
setSubmitting(true);
setTimeout(() => {
setSubmitting(false);
}, 3000)
}
return(
<div className="wrapper">
<h3>How About Them Apples</h3>
{submitting &&
<div>Submtting Form...</div>
}
<form onSubmit={handleSubmit}>
<fieldset>
<label>
<p>Name</p>
<input name="name" />
</label>
</fieldset>
<button type="submit">Submit</button>
</form>
</div>
)
}
export default App;
In addition, you will alert the user that their form is submitting by displaying a short message in the HTML that will display when submitting
is true
.
Save the file. When you do, the browser will reload and you’ll receive the message on submit:
Now you have a basic form that handles the submit event inside the React component. You’ve connected it to your JSX using the onSubmit
event handler and you are using Hooks to conditionally display an alert while the handleSubmit
event is running.
In the next step, you’ll add more user inputs and save the data to state as the user fills out the form.
Step 2 — Collecting Form Data Using Uncontrolled Components
In this step, you’ll collect form data using uncontrolled components. An uncontrolled component is a component that does not have a value
set by React. Instead of setting the data on the component, you’ll connect to the onChange
event to collect the user input. As you build the components, you’ll learn how React handles different input types and how to create a reusable function to collect form data into a single object.
By the end of this step, you’ll be able to build a form using different form elements, including dropdowns and checkboxes. You’ll also be able to collect, submit, and display form data.
Note: In most cases, you’ll use controlled components for your React application. But it’s a good idea to start with uncontrolled components so that you can avoid subtle bugs or accidental loops that you might introduce when incorrectly setting a value.
Currently, you have a form that can submit information, but there is nothing to submit. The form has a single <input>
element, but you are not collecting or storing the data anywhere in the component. In order to be able to store and process the data when the user submits a form, you’ll need to create a way to manage state. You’ll then need to connect to each input using an event handler.
Inside App.js
, use the useReducer
Hook to create a formData
object and a setFormData
function. For the reducer function, pull the name
and value
from the event.target
object and update the state
by spreading the current state while adding the name
and value
at the end. This will create a state object that preserves the current state while overwriting specific values as they change:
form-tutorial/src/components/App/App.js
import React, { useReducer, useState } from 'react';
import './App.css';
const formReducer = (state, event) => {
return {
...state,
[event.target.name]: event.target.value
}
}
function App() {
const [formData, setFormData] = useReducer(formReducer, {});
const [submitting, setSubmitting] = useState(false);
const handleSubmit = event => {
event.preventDefault();
setSubmitting(true);
setTimeout(() => {
setSubmitting(false);
}, 3000)
}
return(
<div className="wrapper">
<h3>How About Them Apples</h3>
{submitting &&
<div>Submtting Form...</div>
}
<form onSubmit={handleSubmit}>
<fieldset>
<label>
<p>Name</p>
<input name="name" onChange={setFormData}/>
</label>
</fieldset>
<button type="submit">Submit</button>
</form>
</div>
)
}
export default App;
After making the reducer, add setFormData
to the onChange
event handler on the input. Save the file. When you do, the browser will reload. However, if you try and type in the input, you’ll get an error:
The problem is that the SyntheticEvent
is reused and cannot be passed to an asynchronous function. In other words, you can’t pass the event directly. To fix this, you’ll need to pull out the data you need before calling the reducer function.
Update the reducer function to take an object with a property of name
and value
. Then create a function called handleChange
that pulls the data from the event.target
and passes the object to setFormData
. Finally, update the onChange
event handler to use the new function:
form-tutorial/src/components/App/App.js
import React, { useReducer, useState } from 'react';
import './App.css';
const formReducer = (state, event) => {<^>
return {
...state,
[event.name]: event.value
}
}
function App() {
const [formData, setFormData] = useReducer(formReducer, {});
const [submitting, setSubmitting] = useState(false);
const handleSubmit = event => {
event.preventDefault();
setSubmitting(true);
setTimeout(() => {
setSubmitting(false);
}, 3000);
}
const handleChange = event => {
setFormData({
name: event.target.name,
value: event.target.value,
});
}
return(
<div className="wrapper">
<h3>How About Them Apples</h3>
{submitting &&
<div>Submtting Form...</div>
}
<form onSubmit={handleSubmit}>
<fieldset>
<label>
<p>Name</p>
<input name="name" onChange={handleChange}/>
</label>
</fieldset>
<button type="submit">Submit</button>
</form>
</div>
)
}
export default App;
Save the file. When you do the page will refresh and you’ll be able to enter data.
Now that you are collecting the form state, update the user display message to show the data in an unordered list (<ul>
) element.
Convert the data to an array using Object.entries
, then map over the data converting each member of the array to an <li>
element with the name and the value. Be sure to use the name
as the key
prop for the element:
form-tutorial/src/components/App/App.js
...
return(
<div className="wrapper">
<h3>How About Them Apples</h3>
{submitting &&
<div>
You are submitting the following:
<ul>
{Object.entries(formData).map(([name, value]) => (
<li key={name}><strong>{name}</strong>:{value.toString()}</li>
))}
</ul>
</div>
}
<form onSubmit={handleSubmit}>
<fieldset>
<label>
<p>Name</p>
<input name="name" onChange={handleChange}/>
</label>
</fieldset>
<button type="submit">Submit</button>
</form>
</div>
)
}
export default App;
Save the file. When you do the page will reload and you’ll be able to enter and submit data:
Now that you have a basic form, you can add more elements. Create another <fieldset>
element and add in a <select>
element with different apple varieties for each <option>
, an <input>
with a type="number"
and a step="1"
to get a count that increments by 1, and an <input>
with a type="checkbox"
for a gift wrapping option.
For each element, add the handleChange
function to the onChange
event handler:
form-tutorial/src/components/App/App.js
...
return(
<div className="wrapper">
<h3>How About Them Apples</h3>
{submitting &&
<div>
You are submitting the following:
<ul>
{Object.entries(formData).map(([name, value]) => (
<li key={name}><strong>{name}</strong>: {value.toString()}</li>
))}
</ul>
</div>
}
<form onSubmit={handleSubmit}>
<fieldset>
<label>
<p>Name</p>
<input name="name" onChange={handleChange}/>
</label>
</fieldset>
<fieldset>
<label>
<p>Apples</p>
<select name="apple" onChange={handleChange}>
<option value="">--Please choose an option--</option>
<option value="fuji">Fuji</option>
<option value="jonathan">Jonathan</option>
<option value="honey-crisp">Honey Crisp</option>
</select>
</label>
<label>
<p>Count</p>
<input type="number" name="count" onChange={handleChange} step="1"/>
</label>
<label>
<p>Gift Wrap</p>
<input type="checkbox" name="gift-wrap" onChange={handleChange} />
</label>
</fieldset>
<button type="submit">Submit</button>
</form>
</div>
)
}
export default App;
Save the file. When you do, the page will reload and you’ll have a variety of input types for your form:
There is one special case here to take into consideration. The value
for the gift wrapping checkbox will always be "on"
, regardless of whether the item is checked or not. Instead of using the event’s value
, you’ll need to use the checked
property.
Update the handleChange
function to see if the event.target.type
is checkbox
. If it is, pass the event.target.checked
property as the value
instead of event.target.value
:
form-tutorial/src/components/App/App.js
import React, { useReducer, useState } from 'react';
import './App.css';
...
function App() {
const [formData, setFormData] = useReducer(formReducer, {});
const [submitting, setSubmitting] = useState(false);
const handleSubmit = event => {
event.preventDefault();
setSubmitting(true);
setTimeout(() => {
setSubmitting(false);
}, 3000);
}
const handleChange = event => {
const isCheckbox = event.target.type === 'checkbox';
setFormData({
name: event.target.name,
value: isCheckbox ? event.target.checked : event.target.value,
})
}
...
In this code, you use the ?
ternary operator to make the conditional statement.
Save the file. After the browser refreshes, fill out the form and click submit. You’ll find that the alert matches the data in the form:
In this step, you learned how to create uncontrolled form components. You saved the form data to a state using the useReducer
Hook and reused that data in different components. You also added different types of form components and adjusted your function to save the correct data depending on the element type.
In the next step, you’ll convert the components to controlled components by dynamically setting the component value.
Step 3 — Updating Form Data Using Controlled Components
In this step, you’ll dynamically set and update data using controlled components. You’ll add a value
prop to each component to set or update the form data. You’ll also reset the form data on submit.
By the end of this step, you’ll be able to dynamically control form data using React state and props.
With uncontrolled components, you don’t have to worry about synchronizing data. Your application will always hold on to the most recent changes. But there are many situations where you’ll need to both read from and write to an input component. To do this, you’ll need the component’s value to be dynamic.
In the previous step, you submitted a form. But after the form submission was successful, the form still contained the old stale data. To erase the data from each input, you’ll need to change the components from uncontrolled components to controlled components.
A controlled component is similar to an uncontrolled component, but React updates the value
prop. The downside is that if you are not careful and do not properly update the value
prop the component will appear broken and won’t seem to update.
In this form, you are already storing the data, so to convert the components, you’ll update the value
prop with data from the formData
state. There is one problem, though: the value
cannot be undefined
. If your value is undefined
, you’ll receive an error in your console.
Since your initial state is an empty object, you’ll need to set the value to be either the value from formData
or a default value such as an empty string. For example, the value for the name would be formData.name || ''
:
form-tutorial/src/components/App/App.js
...
return(
<div className="wrapper">
<h3>How About Them Apples</h3>
{submitting &&
<div>
You are submitting the following:
<ul>
{Object.entries(formData).map(([name, value]) => (
<li key={name}><strong>{name}</strong>: {value.toString()}</li>
))}
</ul>
</div>
}
<form onSubmit={handleSubmit}>
<fieldset>
<label>
<p>Name</p>
<input name="name" onChange={handleChange} value={formData.name || ''}/>
</label>
</fieldset>
<fieldset>
<label>
<p>Apples</p>
<select name="apple" onChange={handleChange} value={formData.apple || ''}>
<option value="">--Please choose an option--</option>
<option value="fuji">Fuji</option>
<option value="jonathan">Jonathan</option>
<option value="honey-crisp">Honey Crisp</option>
</select>
</label>
<label>
<p>Count</p>
<input type="number" name="count" onChange={handleChange} step="1" value={formData.count || ''}/>
</label>
<label>
<p>Gift Wrap</p>
<input type="checkbox" name="gift-wrap" onChange={handleChange} checked={formData['gift-wrap'] || false}/>
</label>
</fieldset>
<button type="submit">Submit</button>
</form>
</div>
)
}
export default App;
As before, the checkbox is a little different. Instead of setting a value, you’ll need to set the checked
attribute. If the attribute is truthy, the browser will show the box as checked. Set the initial checked
attribute to false with formData['gift-wrap'] || false
.
If you want to pre-fill the form, add some default data to the formData
state. Set a default value for the count
by giving formState
a default value of { count: 100 }
. You could also set the default values in the initial object, but you’d need to filter out the falsy values before displaying the form information:
form-tutorial/src/components/App/App.js
...
function App() {
const [formData, setFormData] = useReducer(formReducer, {
count: 100,
});
const [submitting, setSubmitting] = useState(false);
...
Save the file. When you do, the browser will reload and you’ll see the input with the default data:
Note: The value
attribute is different from the placeholder
attribute, which is native on browsers. The placeholder
attribute shows information but will disappear as soon as the user makes a change; it is not stored on the component. You can actively edit the value
, but a placeholder
is just a guide for users.
Now that you have active components, you can clear the data on submit. To do so, add a new condition in your formReducer
. If event.reset
is truthy, return an object with empty values for each form element. Be sure to add a value for each input. If you return an empty object or an incomplete object, the components will not update since the value is undefined
.
After you add the new event condition in the formReducer
, update your submit function to reset the state when the function resolves:
form-tutorial/src/components/App/App.js
import React, { useReducer, useState } from 'react';
import './App.css';
const formReducer = (state, event) => {
if(event.reset) {
return {
apple: '',
count: 0,
name: '',
'gift-wrap': false,
}
}
return {
...state,
[event.name]: event.value
}
}
function App() {
const [formData, setFormData] = useReducer(formReducer, {
count: 100
});
const [submitting, setSubmitting] = useState(false);
const handleSubmit = event => {
event.preventDefault();
setSubmitting(true);
setTimeout(() => {
setSubmitting(false);
setFormData({
reset: true
})
}, 3000);
}
...
Save the file. When you do, the browser will reload and the form will clear on submit.
In this step, you converted your uncontrolled components to controlled components by setting the value
or the checked
attributes dynamically. You also learned how to refill data by setting a default state and how to clear the data by updating the form reducer to return default values.
In this next step, you’ll set form component properties dynamically and disable a form while it is submitting.
Step 4 — Dynamically Updating Form Properties
In this step, you’ll dynamically update form element properties. You’ll set properties based on previous choices and disable your form during submit to prevent accidental multiple submissions.
Currently, each component is static. They do not change as the form changes. In most applications, forms are dynamic. Fields will change based on the previous data. They’ll validate and show errors. They may disappear or expand as you fill in other components.
Like most React components, you can dynamically set properties and attributes on components and they will re-render as the data changes.
Try setting an input to be disabled
until a condition is met by another input. Update the gift wrapping checkbox to be disabled unless the user selects the fuji
option.
Inside App.js
, add the disabled
attribute to the checkbox. Make the property truthy if the formData.apple
is fuji
:
form-tutorial/src/components/App/App.js
...
<fieldset>
<label>
<p>Apples</p>
<select name="apple" onChange={handleChange} value={formData.apple || ''}>
<option value="">--Please choose an option--</option>
<option value="fuji">Fuji</option>
<option value="jonathan">Jonathan</option>
<option value="honey-crisp">Honey Crisp</option>
</select>
</label>
<label>
<p>Count</p>
<input type="number" name="count" onChange={handleChange} step="1" value={formData.count || ''}/>
</label>
<label>
<p>Gift Wrap</p>
<input
checked={formData['gift-wrap'] || false}
disabled={formData.apple !== 'fuji'}
name="gift-wrap"
onChange={handleChange}
type="checkbox"
/>
</label>
</fieldset>
<button type="submit">Submit</button>
</form>
</div>
)
}
export default App;
Save the file. When you do, the browser will reload and the checkbox will be disabled by default:
If you select the apple type of Fuji, the element will be enabled:
In addition to changing properties on individual components, you can modify entire groups of components by updating the fieldset
component.
As an example, you can disable the form while the form is actively submitting. This will prevent double submissions and prevent the user from changing fields before the handleSubmit
function fully resolves.
Add disabled={submitting}
to each <fieldset>
element and the <button>
element:
form-tutorial/src/components/App/App.js
...
<form onSubmit={handleSubmit}>
<fieldset disabled={submitting}>
<label>
<p>Name</p>
<input name="name" onChange={handleChange} value={formData.name || ''}/>
</label>
</fieldset>
<fieldset disabled={submitting}>
<label>
<p>Apples</p>
<select name="apple" onChange={handleChange} value={formData.apple || ''}>
<option value="">--Please choose an option--</option>
<option value="fuji">Fuji</option>
<option value="jonathan">Jonathan</option>
<option value="honey-crisp">Honey Crisp</option>
</select>
</label>
<label>
<p>Count</p>
<input type="number" name="count" onChange={handleChange} step="1" value={formData.count || ''}/>
</label>
<label>
<p>Gift Wrap</p>
<input
checked={formData['gift-wrap'] || false}
disabled={formData.apple !== 'fuji'}
name="gift-wrap"
onChange={handleChange}
type="checkbox"
/>
</label>
</fieldset>
<button type="submit" disabled={submitting}>Submit</button>
</form>
</div>
)
}
export default App;
Save the file, and the browser will refresh. When you submit the form, the fields will be disabled until the submitting function resolves:
You can update any attribute on an input component. This is helpful if you need to change the maxvalue
for a number input or if you need to add a dynamic pattern
attribute for validation.
In this step, you dynamically set attributes on form components. You added a property to dynamically enable or disable a component based on the input from another component and you disabled entire sections using the <fieldset>
component.
Conclusion
Forms are key to rich web applications. In React, you have different options for connecting and controlling forms and elements. Like other components, you can dynamically update properties including the value
input elements. Uncontrolled components are best for simplicity, but might not fit situations when a component needs to be cleared or pre-populated with data. Controlled components give you more opportunities to update the data, but can add another level of abstraction that may cause unintentional bugs or re-renders.
Regardless of your approach, React gives you the ability to dynamically update and adapt your forms to the needs of your application and your users.
If you would like to read more React tutorials, check out our React Topic page, or return to the How To Code in React.js series page.
https://mattboldt.com/2020/05/02/formdata-with-react-hooks-and-fetch/
###FormData with React Hooks and Fetch
May 02, 2020
Forms are hard. Forms in React are hard. For most cases, a 3rd party solution like react-hooks-form is the way to go. However, some may find themselves in a position where they cannot (or prefer not to) add a dependency, and simply want submit a no-frills html form, asynchronously, using only React.
Using setState
Consider the following plain React form:
const UserForm = props => {
const [user, setUser] = useState(props.user)
const submit = e => {
e.preventDefault()
fetch('/api', {
method: 'POST',
body: JSON.stringify({ user }),
headers: { 'Content-Type': 'application/json' },
})
.then(res => res.json())
.then(json => setUser(json.user))
}
return (
<form onSubmit={submit}>
<input
type="text"
name="user[name]"
value={user.name}
onChange={e => setUser({ ...user, name: e.target.value })}
/>
{user.errors.name && <p>{user.errors.name}</p>}
<input
type="email"
name="user[email]"
value={user.email}
onChange={e => setUser({ ...user, email: e.target.value })}
/>
{user.errors.name && <p>{user.errors.name}</p>}
<input type="submit" name="Sign Up" />
</form>
)
}
- We're using
useState
to keep track of our state both before and after submission. Without this hook, we wouldn't know what to send to the API. - We must use controlled components with
onChange
handlers for each input, even though it's not necessary to keep track of changes for every keystroke. - Even though we're using a form element and inputs, we're not reaping any benefits from the built-in attributes like
name
.
Using FormData
Here's the same form with FormData
const UserForm = props => {
const [user, setUser] = useState(props.user)
const form = useRef(null)
const submit = e => {
e.preventDefault()
const data = new FormData(form.current)
fetch('/api', { method: 'POST', body: data })
.then(res => res.json())
.then(json => setUser(json.user))
}
return (
<form ref={form} onSubmit={submit}>
<input type="text" name="user[name]" defaultValue={user.name} />
{user.errors.name && <p>{user.errors.name}</p>}
<input type="email" name="user[email]" defaultValue={user.email} />
{user.errors.email && <p>{user.errors.email}</p>}
<input type="submit" name="Sign Up" />
</form>
)
}
- Instead of sending data as JSON, we're sending it as a standard
FormData
object for the server to consume. - There was no need to use controlled inputs, so instead we're using
defaultValue
and letting that state remain unbound. This also means we don't need to update the stateonChange
. - We're letting each input's
name
attribute build up what gets sent to the API, instead of building an object and serializing JSON manually. setUser
is still called after the API responds, which can be helpful if we needed to attach errors from the API response.
A More Complex Example
What if our sign up form grows, and we need to add address fields? Using setState
alone like in the first example could prove challenging with updating all that state and preparing it to be sent to the API. But with FormData
, it becomes rather easy:
const UserForm = props => {
const [user, setUser] = useState(props.user)
const form = useRef(null)
const submit = e => {
e.preventDefault()
const data = new FormData(form.current)
fetch('/api', {
method: 'POST',
body: data,
})
.then(res => res.json())
.then(json => setUser(json.user))
}
return (
<form ref={form} onSubmit={submit}>
<input type="text" name="user[name]" defaultValue={user.name} />
{user.errors.name && <p>{user.errors.name}</p>}
<input type="email" name="user[email]" defaultValue={user.email} />
{user.errors.email && <p>{user.errors.email}</p>}
{user.addresses.map((address, idx) => (
<div key={idx}>
<input
type="text"
name="user[addresses][][address]"
defaultValue={address.address}
/>
<input
type="text"
name="user[addresses][][city]"
defaultValue={address.city}
/>
<input
type="text"
name="user[addresses][][state]"
defaultValue={address.state}
/>
{user.errors.addresses[idx] && <p>{user.errors.addresses[idx]}</p>}
</div>
))}
<input type="submit" name="Sign Up" />
</form>
)
}
- No changes to state were necessary to include our new address fields. They live in uncontrolled inputs, and required no complex state traversing or merging strategies to keep everything in sync.
- Since our API (in my case a very small node Express server) knows how to parse
FormData
already, it can access the new fields just as easily as if it were JSON. - The state structure is tightly coupled to our form element, making it easier for another developer to come in and understand what's going on, add fields, or perform validation. They don't need to worry as much about the state or when/where it's updated.
Error when POST
ing multipart/form-data
One issue I did run into was the setting of custom headers using fetch
. Note the absence of a Content-Type
header in the API request:
const submit = e => {
e.preventDefault()
const data = new FormData(form.current)
fetch('/api', {
method: 'POST',
headers: new Headers({
'AuthHeader': '123',
// STOP! Do not add the following header!
// 'Content-Type': 'multipart/form-data'
}),
body: data,
})
.then(res => res.json())
.then(json => setUser(json.user))
}
After some time debugging, I found this Github issue for fetch
. Essentially, when you post FormData
via fetch
, it automatically appends this header with some extra info. It looks something like this:
Content-Type: multipart/form-data;boundary=----WebKitFormBoundaryyrV7KO0BoCBuDbTL
Overriding this header results in a failed POST
request, and a Bad Time™. So remember to exclude it when using FormData
with fetch
.
Using Reducer
https://www.codingdeft.com/posts/react-form-validation/https://www.codingdeft.com/posts/react-form-validation/
Table of Contents
Form Validation Library Comparison
Setting up the project
Form Validation Library Comparison
There are a lot of libraries out there for validating forms in react. Redux-Form, Formik, react-final-form are few among them.
While these libraries are cool and they help in validating the forms to a great extent, they come with a catch: they add up to bundle size. Let's see a quick comparison between these libraries:
Redux Form
Redux form cannot function on its own. It has 2 additional dependencies redux and react-redux. If you are already using redux in your application, then you have already installed redux and react-redux packages. You can see that from the bundle phobia analysis given below that it adds 35 kB to your bundle size, while react itself is just about 38.5 kB.
Formik
Formik can function on its own without any additional packages to be installed along with it. The bundle size is 15 kB, which is considerably smaller than that of redux-form.
React Final Form
React final form is created by the author (@erikras) of redux-form. It is a wrapper around the final-form core, which has no dependencies. Since one of the goals behind react final forms was to reduce bundle size, it weighs 8.5 kB gzipped.
Now let's see how we can do form validation without depending upon these libraries:
Setting up the project
Create a new react project using the following command:
1npx create-react-app react-form-validation
Update App.js
with the following code:
App.js
1import React from "react"
2import "./App.css"
3
4function App() {
5 return (
6 <div className="App">
7 <h1 className="title">Sign Up</h1>
8 <form>
9 <div className="input_wrapper">
10 <label htmlFor="name">Name:</label>
11 <input type="text" name="name" id="name" />
12 </div>
13 <div className="input_wrapper">
14 <label htmlFor="email">Email:</label>
15 <input type="email" name="email" id="email" />
16 </div>
17 <div className="input_wrapper">
18 <label htmlFor="password">Password:</label>
19 <input type="password" name="password" id="password" />
20 </div>
21 <div className="input_wrapper">
22 <label htmlFor="mobile">Mobile:</label>
23 <input type="text" name="mobile" id="mobile" />
24 </div>
25 <div className="input_wrapper">
26 <label className="toc">
27 <input type="checkbox" name="terms" /> Accept terms and conditions
28 </label>
29 </div>
30 <div className="input_wrapper">
31 <input className="submit_btn" type="submit" value="Sign Up" />
32 </div>
33 </form>
34 </div>
35 )
36}
37
38export default App
Here we have created a simple sign up form with few fields. Now to style these fields let's add some css:
App.css
1.App {
2 max-width: 300px;
3 margin: 1rem auto;
4}
5.title {
6 text-align: center;
7}
8.input_wrapper {
9 display: flex;
10 flex-direction: column;
11 margin-bottom: 0.5rem;
12}
13.input_wrapper label {
14 font-size: 1.1rem;
15}
16.input_wrapper input {
17 margin-top: 0.4rem;
18 font-size: 1.1rem;
19}
20.submit_btn {
21 cursor: pointer;
22 padding: 0.2rem;
23}
24.toc,
25.toc input {
26 cursor: pointer;
27}
Now if you open the app, you should see our basic form set up:
Binding the form value with the state
Now that we have the form ready, let's bind the input values with the state
App.js
1import React, { useReducer } from "react"
2import "./App.css"
3
4/**
5 * The initial state of the form
6 * value: stores the value of the input field
7 * touched: indicates whether the user has tried to input anything in the field
8 * hasError: determines whether the field has error.
9 * Defaulted to true since all fields are mandatory and are empty on page load.
10 * error: stores the error message
11 * isFormValid: Stores the validity of the form at any given time.
12 */
13const initialState = {
14 name: { value: "", touched: false, hasError: true, error: "" },
15 email: { value: "", touched: false, hasError: true, error: "" },
16 password: { value: "", touched: false, hasError: true, error: "" },
17 mobile: { value: "", touched: false, hasError: true, error: "" },
18 terms: { value: false, touched: false, hasError: true, error: "" },
19 isFormValid: false,
20}
21
22/**
23 * Reducer which will perform form state update
24 */
25const formsReducer = (state, action) => {
26 return state
27}
28
29function App() {
30 const [formState, dispatch] = useReducer(formsReducer, initialState)
31
32 return (
33 <div className="App">
34 <h1 className="title">Sign Up</h1>
35 <form>
36 <div className="input_wrapper">
37 <label htmlFor="name">Name:</label>
38 <input
39 type="text"
40 name="name"
41 id="name"
42 value={formState.name.value}
43 />
44 </div>
45 <div className="input_wrapper">
46 <label htmlFor="email">Email:</label>
47 <input
48 type="email"
49 name="email"
50 id="email"
51 value={formState.email.value}
52 />
53 </div>
54 <div className="input_wrapper">
55 <label htmlFor="password">Password:</label>
56 <input
57 type="password"
58 name="password"
59 id="password"
60 value={formState.password.value}
61 />
62 </div>
63 <div className="input_wrapper">
64 <label htmlFor="mobile">Mobile:</label>
65 <input
66 type="text"
67 name="mobile"
68 id="mobile"
69 value={formState.mobile.value}
70 />
71 </div>
72 <div className="input_wrapper">
73 <label className="toc">
74 <input
75 type="checkbox"
76 name="terms"
77 checked={formState.terms.value}
78 />{" "}
79 Accept terms and conditions
80 </label>
81 </div>
82 <div className="input_wrapper">
83 <input className="submit_btn" type="submit" value="Sign Up" />
84 </div>
85 </form>
86 </div>
87 )
88}
89
90export default App
In the above code,
- We have introduced a new object
initialState
, which stores the initial state of the form. - We also have defined a reducer function named
formsReducer
, which does nothing as of now, but we will have the logic inside it to update the form state. - We have introduced
useReducer
hook, which returns the current form state and a dispatch function, which will be used to fire form update actions.
If you try to enter any values in the form now, you will not be able to update it because we don't have any handler functions, which will update our state.
Adding form handler
Create a folder called lib
in src
directory and a file named formUtils.js
inside it. This file will have the handler functions which can be reused for other forms.
formUtils.js
1export const UPDATE_FORM = "UPDATE_FORM"
2
3/**
4 * Triggered every time the value of the form changes
5 */
6export const onInputChange = (name, value, dispatch, formState) => {
7 dispatch({
8 type: UPDATE_FORM,
9 data: {
10 name,
11 value,
12 hasError: false,
13 error: "",
14 touched: false,
15 isFormValid: true,
16 },
17 })
18}
Here you could see that we are dispatching the UPDATE_FORM
action with the value that is being passed to the handler. As of now, we are setting hasError
to false
and isFormValid
to true
since we are yet to write the validation logic.
Now in the App.js
file, update the reducer function to handle the UPDATE_FORM
action. Here we are updating the value of the corresponding input field using the name
as the key.
formReducer function
1//...
2
3import { UPDATE_FORM, onInputChange } from "./lib/formUtils"
4
5//...
6const formsReducer = (state, action) => {
7 switch (action.type) {
8 case UPDATE_FORM:
9 const { name, value, hasError, error, touched, isFormValid } = action.data
10 return {
11 ...state,
12 // update the state of the particular field,
13 // by retaining the state of other fields
14 [name]: { ...state[name], value, hasError, error, touched },
15 isFormValid,
16 }
17 default:
18 return state
19 }
20}
Now bind the onInputChange
handler we imported above with the input field for name:
1<div className="input_wrapper">
2 <label htmlFor="name">Name:</label>
3 <input
4 type="text"
5 name="name"
6 id="name"
7 value={formState.name.value}
8 onChange={e => {
9 onInputChange("name", e.target.value, dispatch, formState)
10 }}
11 />
12</div>
Now you should be able to edit the name field. Now its time to write the validation logic!
Adding Validations
Add a function called validateInput
to formUtils.js
. Inside this function, we will write validations for all the fields.
1export const validateInput = (name, value) => {
2 let hasError = false,
3 error = ""
4 switch (name) {
5 case "name":
6 if (value.trim() === "") {
7 hasError = true
8 error = "Name cannot be empty"
9 } else if (!/^[a-zA-Z ]+$/.test(value)) {
10 hasError = true
11 error = "Invalid Name. Avoid Special characters"
12 } else {
13 hasError = false
14 error = ""
15 }
16 break
17 default:
18 break
19 }
20 return { hasError, error }
21}
Here you can see that in the first if condition, we are checking for empty value since name field is mandatory. In the second if condition, we are using RegEx to validate if the name contains any other characters other than the English alphabets and spaces.
We assume that all the names are in English. If you have a user base in regions where the name contains characters outside the English alphabet, you can update the Regex accordingly.
Now update the onInputChange
function to make use of the validation function:
1export const onInputChange = (name, value, dispatch, formState) => {
2 const { hasError, error } = validateInput(name, value)
3 let isFormValid = true
4
5 for (const key in formState) {
6 const item = formState[key]
7 // Check if the current field has error
8 if (key === name && hasError) {
9 isFormValid = false
10 break
11 } else if (key !== name && item.hasError) {
12 // Check if any other field has error
13 isFormValid = false
14 break
15 }
16 }
17
18 dispatch({
19 type: UPDATE_FORM,
20 data: { name, value, hasError, error, touched: false, isFormValid },
21 })
22}
You will also see that we are looping through the formState
to check if any of the field is having error to determine the overall validity of the form.
Now let's see if our validation logic works fine. Since we aren't displaying the error message yet, let's log the formState
and see the values.
When an invalid name is entered
Use
console.table({"name state": formState.name});
for displaying the values of an object in tabular format
When the name is kept empty
When a valid name is entered
You might see that the loggers are getting printed twice each time you type a character: this is expected to happen in Strict Mode in the development environment. This is actually to make sure that there are no side effects inside the reducer functions.
Displaying error message
Before showing the error message, let's add another handler function to our formUtils.js
1//...
2export const onFocusOut = (name, value, dispatch, formState) => {
3 const { hasError, error } = validateInput(name, value)
4 let isFormValid = true
5 for (const key in formState) {
6 const item = formState[key]
7 if (key === name && hasError) {
8 isFormValid = false
9 break
10 } else if (key !== name && item.hasError) {
11 isFormValid = false
12 break
13 }
14 }
15
16 dispatch({
17 type: UPDATE_FORM,
18 data: { name, value, hasError, error, touched: true, isFormValid },
19 })
20}
You might observe that the onFocusOut
function is very similar to onInputChange
, except that we pass touched
as true
in case of onFocusOut
. The reason for having additional handler function, which will be bound with the onBlur
event of the input is to show the error messages only when the user finishes typing and moves to the next field.
Now that we have the error message stored in our state, let's display it:
App.js
1//...
2import { UPDATE_FORM, onInputChange, onFocusOut } from "./lib/formUtils"
3
4//...
5function App() {
6 const [formState, dispatch] = useReducer(formsReducer, initialState)
7
8 return (
9 <div className="App">
10 <h1 className="title">Sign Up</h1>
11 <form>
12 <div className="input_wrapper">
13 <label htmlFor="name">Name:</label>
14 <input
15 type="text"
16 name="name"
17 id="name"
18 value={formState.name.value}
19 onChange={e => {
20 onInputChange("name", e.target.value, dispatch, formState)
21 }}
22 onBlur={e => {
23 onFocusOut("name", e.target.value, dispatch, formState)
24 }}
25 />
26 {formState.name.touched && formState.name.hasError && (
27 <div className="error">{formState.name.error}</div>
28 )}
29 </div>
30 {/* ... */}
31 </form>
32 </div>
33 )
34}
35
36export default App
You will see that we have added onBlur
handler and we are displaying the error message whenever the form is touched and has errors.
Now let's add some styling for the error message
App.css
1/*...*/
2.error {
3 margin-top: 0.25rem;
4 color: #f65157;
5}
Now if you type an invalid name or leave the field empty, you will see the error message:
Adding validation to other fields
Now let's add validation to other fields
Update the validateInput
function inside formUtils.js
:
formUtils.js
1export const validateInput = (name, value) => {
2 let hasError = false,
3 error = ""
4 switch (name) {
5 case "name":
6 if (value.trim() === "") {
7 hasError = true
8 error = "Name cannot be empty"
9 } else if (!/^[a-zA-Z ]+$/.test(value)) {
10 hasError = true
11 error = "Invalid Name. Avoid Special characters"
12 } else {
13 hasError = false
14 error = ""
15 }
16 break
17 case "email":
18 if (value.trim() === "") {
19 hasError = true
20 error = "Email cannot be empty"
21 } else if (
22 !/^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(
23 value
24 )
25 ) {
26 hasError = true
27 error = "Invalid Email"
28 } else {
29 hasError = false
30 error = ""
31 }
32 break
33 case "password":
34 if (value.trim() === "") {
35 hasError = true
36 error = "Password cannot be empty"
37 } else if (value.trim().length < 8) {
38 hasError = true
39 error = "Password must have at least 8 characters"
40 } else {
41 hasError = false
42 error = ""
43 }
44 break
45 case "mobile":
46 if (value.trim() === "") {
47 hasError = true
48 error = "Mobile cannot be empty"
49 } else if (!/^[0-9]{10}$/.test(value)) {
50 hasError = true
51 error = "Invalid Mobile Number. Use 10 digits only"
52 } else {
53 hasError = false
54 error = ""
55 }
56 break
57 case "terms":
58 if (!value) {
59 hasError = true
60 error = "You must accept terms and conditions"
61 } else {
62 hasError = false
63 error = ""
64 }
65 break
66 default:
67 break
68 }
69 return { hasError, error }
70}
Note that we have added validation password to have minimum of 8 characters, mobile number to have 10 digits. Also, you might be wondering about the really long RegEx used for email validation. You can read more about email validation at emailregex.com.
Now let's bind them to the form:
App.js
1//...
2
3function App() {
4 const [formState, dispatch] = useReducer(formsReducer, initialState)
5
6 return (
7 <div className="App">
8 <h1 className="title">Sign Up</h1>
9 <form>
10 <div className="input_wrapper">
11 <label htmlFor="name">Name:</label>
12 <input
13 type="text"
14 name="name"
15 id="name"
16 value={formState.name.value}
17 onChange={e => {
18 onInputChange("name", e.target.value, dispatch, formState)
19 }}
20 onBlur={e => {
21 onFocusOut("name", e.target.value, dispatch, formState)
22 }}
23 />
24 {formState.name.touched && formState.name.hasError && (
25 <div className="error">{formState.name.error}</div>
26 )}
27 </div>
28 <div className="input_wrapper">
29 <label htmlFor="email">Email:</label>
30 <input
31 type="email"
32 name="email"
33 id="email"
34 value={formState.email.value}
35 onChange={e => {
36 onInputChange("email", e.target.value, dispatch, formState)
37 }}
38 onBlur={e => {
39 onFocusOut("email", e.target.value, dispatch, formState)
40 }}
41 />
42 {formState.email.touched && formState.email.hasError && (
43 <div className="error">{formState.email.error}</div>
44 )}
45 </div>
46 <div className="input_wrapper">
47 <label htmlFor="password">Password:</label>
48 <input
49 type="password"
50 name="password"
51 id="password"
52 value={formState.password.value}
53 onChange={e => {
54 onInputChange("password", e.target.value, dispatch, formState)
55 }}
56 onBlur={e => {
57 onFocusOut("password", e.target.value, dispatch, formState)
58 }}
59 />
60 {formState.password.touched && formState.password.hasError && (
61 <div className="error">{formState.password.error}</div>
62 )}
63 </div>
64 <div className="input_wrapper">
65 <label htmlFor="mobile">Mobile:</label>
66 <input
67 type="text"
68 name="mobile"
69 id="mobile"
70 value={formState.mobile.value}
71 onChange={e => {
72 onInputChange("mobile", e.target.value, dispatch, formState)
73 }}
74 onBlur={e => {
75 onFocusOut("mobile", e.target.value, dispatch, formState)
76 }}
77 />
78 {formState.mobile.touched && formState.mobile.hasError && (
79 <div className="error">{formState.mobile.error}</div>
80 )}
81 </div>
82 <div className="input_wrapper">
83 <label className="toc">
84 <input
85 type="checkbox"
86 name="terms"
87 checked={formState.terms.value}
88 onChange={e => {
89 onFocusOut("terms", e.target.checked, dispatch, formState)
90 }}
91 />
92 Accept terms and conditions
93 </label>
94 {formState.terms.touched && formState.terms.hasError && (
95 <div className="error">{formState.terms.error}</div>
96 )}
97 </div>
98 <div className="input_wrapper">
99 <input className="submit_btn" type="submit" value="Sign Up" />
100 </div>
101 </form>
102 </div>
103 )
104}
105
106export default App
Now if you test the application, you will see all validations in place:
Though we have all the validations, we are not validating the form if the user clicks on submit without filling any of the fields.
Adding form level validation
For the last time, let's add the form level validation
App.js
1import React, { useReducer, useState } from "react"
2import "./App.css"
3import {
4 UPDATE_FORM,
5 onInputChange,
6 onFocusOut,
7 validateInput,
8} from "./lib/formUtils"
9
10//...
11
12function App() {
13 const [formState, dispatch] = useReducer(formsReducer, initialState)
14
15 const [showError, setShowError] = useState(false)
16
17 const formSubmitHandler = e => {
18 e.preventDefault() //prevents the form from submitting
19
20 let isFormValid = true
21
22 for (const name in formState) {
23 const item = formState[name]
24 const { value } = item
25 const { hasError, error } = validateInput(name, value)
26 if (hasError) {
27 isFormValid = false
28 }
29 if (name) {
30 dispatch({
31 type: UPDATE_FORM,
32 data: {
33 name,
34 value,
35 hasError,
36 error,
37 touched: true,
38 isFormValid,
39 },
40 })
41 }
42 }
43 if (!isFormValid) {
44 setShowError(true)
45 } else {
46 //Logic to submit the form to backend
47 }
48
49 // Hide the error message after 5 seconds
50 setTimeout(() => {
51 setShowError(false)
52 }, 5000)
53 }
54
55 return (
56 <div className="App">
57 <h1 className="title">Sign Up</h1>
58 {showError && !formState.isFormValid && (
59 <div className="form_error">Please fill all the fields correctly</div>
60 )}
61 <form onSubmit={e => formSubmitHandler(e)}>
62 <div className="input_wrapper">{/* ... */}</div>
63 </form>
64 </div>
65 )
66}
67
68export default App
We have added a block error message which will be displayed when the user submits the form and as long as the form is invalid.
Let's add some css to style the error message:
App.css
1/* ... */
2
3.form_error {
4 color: #721c24;
5 background-color: #f8d7da;
6 border-color: #f5c6cb;
7 padding: 0.5rem 1.25rem;
8 border: 1px solid transparent;
9 border-radius: 0.25rem;
10 margin: 1rem 0;
11}
Now if you click on the submit button without filling the form you should see:
Analyzing the bundle size
Let's see if we were successful in reducing the bundle size by writing our own implementation of form validation. To do so, first install webpack-bundle-analyzer
package as a dev dependency:
1yarn add webpack-bundle-analyzer -D
Create a file named analyze.js
in the root directory with the following content:
analyze.js
1// script to enable webpack-bundle-analyzer
2process.env.NODE_ENV = "production"
3const webpack = require("webpack")
4const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
5 .BundleAnalyzerPlugin
6const webpackConfigProd = require("react-scripts/config/webpack.config")(
7 "production"
8)
9
10webpackConfigProd.plugins.push(new BundleAnalyzerPlugin())
11
12// actually running compilation and waiting for plugin to start explorer
13webpack(webpackConfigProd, (err, stats) => {
14 if (err || stats.hasErrors()) {
15 console.error(err)
16 }
17})
Run the following command in the terminal:
1node analyze.js
Now a browser window will automatically open with the URL http://127.0.0.1:8888
If you see the bundle size, you will find that our application including form validation utils and css is just 1.67kB gzipped!
Conclusion
While the form validation libraries have a lot of advantages like it lets you write less code and if there are a lot of forms in your application, it pays for itself. But if you are having a simple form and you are concerned about bundle size you can always go for this custom implementation. Also, if the form is very complex, then again you will have to go for custom implementation since the form validation libraries might not cover all your use cases.
Source code and Demo
You can view the complete source code here and a demo here.
Simple Form Validation
https://stackoverflow.com/questions/41296668/reactjs-form-input-validationOn every change, update the state for the changed field.
Then you can easily check if that field is empty or whatever else you want.
You could do something as follows :
class Test extends React.Component {
constructor(props) {
super(props);
this.state = {
fields: {},
errors: {},
};
}
handleValidation() {
let fields = this.state.fields;
let errors = {};
let formIsValid = true;
//Name
if (!fields["name"]) {
formIsValid = false;
errors["name"] = "Cannot be empty";
}
if (typeof fields["name"] !== "undefined") {
if (!fields["name"].match(/^[a-zA-Z]+$/)) {
formIsValid = false;
errors["name"] = "Only letters";
}
}
//Email
if (!fields["email"]) {
formIsValid = false;
errors["email"] = "Cannot be empty";
}
if (typeof fields["email"] !== "undefined") {
let lastAtPos = fields["email"].lastIndexOf("@");
let lastDotPos = fields["email"].lastIndexOf(".");
if (
!(
lastAtPos < lastDotPos &&
lastAtPos > 0 &&
fields["email"].indexOf("@@") == -1 &&
lastDotPos > 2 &&
fields["email"].length - lastDotPos > 2
)
) {
formIsValid = false;
errors["email"] = "Email is not valid";
}
}
this.setState({ errors: errors });
return formIsValid;
}
contactSubmit(e) {
e.preventDefault();
if (this.handleValidation()) {
alert("Form submitted");
} else {
alert("Form has errors.");
}
}
handleChange(field, e) {
let fields = this.state.fields;
fields[field] = e.target.value;
this.setState({ fields });
}
render() {
return (
);
}
}
React.render( , document.getElementById("container"));
In this example I did the validation only for email and name, but you have an idea how to do it. For the rest you can do it self.
There is maybe a better way, but you will get the idea.
Here is fiddle.
https://www.techiediaries.com/php-react-rest-api-crud-tutorial/
HP, MySQL & React REST API Tutorial with Example Form
Throughout this tutorial, we'll be using PHP with React and Axios to create a simple REST API application with CRUD operations. In the backend we'll use PHP with a MySQL database.
The PHP backend will expose a set of RESTful API endpoints so we'll be using the Axios library for making Ajax calls from the React.js UI.
We'll also see how to handle forms in React and how to send multipart form data with Axios using FormData.
In this tutorial, we are going to integrate React with PHP using Babel in the browser and a <script>
tag. As such, we'll serve the React application from PHP so we don't need to enable CORS in our server since both the backend and frontend are served from the same domain.
We'll see the other approach of using two separate servers for the frontend and backend apps in another tutorial which will use the create-react-app
to create the React project.
Prerequisites
You must have the following prerequsites in order to follow this tutorial comfortably:
- Knowledge of PHP and MySQL,
- Knowledge of JavaScript and React,
- PHP and MySQL installed on your development machine.
Creating the MySQL Database
Let's start by creating a MySQL database using the MySQL client (this usually gets installed when you install the MySQL server). Open a new terminal and run the following command:
mysql -u root -p
You'll be asked for your MySQL password. Make sure to submit the correct password and type Enter on your keyboard to confirm.
Next, you'll be presetend with the MySQL client CLI. You can create a database using the following SQL statement:
mysql> create database reactdb;
Next, let's add a SQL table in our database. Simpy run the following SQL instructions:
mysql> use reactdb;
mysql> CREATE TABLE `contacts` (
`id` int(11) NOT NULL PRIMARY KEY AUTO_INCREMENT,
`name` varchar(100) NOT NULL,
`email` varchar(100) NOT NULL,
`city` varchar(100),
`country` varchar(100),
`job` varchar(100)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
We first run the use
SQL instruction to select the reactdb
database as our current working database. Next, we invoke the CREATE TABLE <name_of_table>
statement to create a SQL table that has the following columns:
- id: A unique identifier for the person,
- name: The name of the person,
- email: The email for the person,
- city: The city of the person
- country: The country of the person
- job: The job occupied by the person
Basically, this is a simple database for managing your contacts data.
Creating The PHP & MySQL RESTful API
After creating the MySQL database, table and columns. Let's now proceed to create a RESTful API interface exposed by a PHP application that runs CRUD operations against our previously-created MySQL table. Head back to your terminal and start by creating a directory for your project's files:
$ cd ~
$ mkdir php-react-rest-api-crud
Create a REST API Endpoint
Now, let's create an endpoint that provides contacts data in a JSON format to our Vue frontend.
Create an api
folder inside your project's root folder:
$ mkdir api
Navigate inside the api
folder and create a contacts.php
file and add the following content:
<?php
$host = "localhost";
$user = "root";
$password = "YOUR_MYSQL_DB_PASSWORD";
$dbname = "reactdb";
$id = '';
$con = mysqli_connect($host, $user, $password,$dbname);
$method = $_SERVER['REQUEST_METHOD'];
$request = explode('/', trim($_SERVER['PATH_INFO'],'/'));
if (!$con) {
die("Connection failed: " . mysqli_connect_error());
}
switch ($method) {
case 'GET':
$id = $_GET['id'];
$sql = "select * from contacts".($id?" where id=$id":'');
break;
case 'POST':
$name = $_POST["name"];
$email = $_POST["email"];
$country = $_POST["country"];
$city = $_POST["city"];
$job = $_POST["job"];
$sql = "insert into contacts (name, email, city, country, job) values ('$name', '$email', '$city', '$country', '$job')";
break;
}
// run SQL statement
$result = mysqli_query($con,$sql);
// die if SQL statement failed
if (!$result) {
http_response_code(404);
die(mysqli_error($con));
}
if ($method == 'GET') {
if (!$id) echo '[';
for ($i=0 ; $i<mysqli_num_rows($result) ; $i++) {
echo ($i>0?',':'').json_encode(mysqli_fetch_object($result));
}
if (!$id) echo ']';
} elseif ($method == 'POST') {
echo json_encode($result);
} else {
echo mysqli_affected_rows($con);
}
$con->close();
We first use the MySQLi PHP extension to create a connection to our MySQL database using the mysqli_connect()
method. Next, we use the $_SERVER['REQUEST_METHOD']
to retrieve the request method sent from the Axios client. If the request is GET, we create a SQL SELECT
query. if the request is POST we create a SQL INSERT
query with the post data retrieved from the $_POST
object.
After that, we use the mysqli_query()
method to run the query against our database table either to get or create data. Finally we use the json_encode()
method to encode data as JSON data and send it to the client.
You can serve your PHP application using the following command from the root of your project:
$ php -S 127.0.0.1:8080
Create the React App
Next, navigate to the project's root folder and add an index.php
file:
$ touch index.php
Next, open the index.php
file and add the following code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>PHP| MySQL | React.js | Axios Example</title>
<script src= "https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script src= "https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<!-- Load Babel Compiler -->
<script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
</body>
</html>
We simply include the React, ReactDOM, Babel and Axios libraries from their CDNs.
Next, in the index.html
, in the <body>
tag add a <div>
tag where you can mount your React application:
<div id='root'></div>
Next, add a <script>
tag of the text/babel
type to create our React app:
<body>
<div id='root'></div>
<script type="text/babel">
class App extends React.Component {
state = {
contacts: []
}
render() {
return (
<React.Fragment>
<h1>Contact Management</h1>
<table border='1' width='100%' >
<tr>
<th>Name</th>
<th>Email</th>
<th>Country</th>
<th>City</th>
<th>Job</th>
</tr>
{this.state.contacts.map((contact) => (
<tr>
<td>{ contact.name }</td>
<td>{ contact.email }</td>
<td>{ contact.country }</td>
<td>{ contact.city }</td>
<td>{ contact.job }</td>
</tr>
))}
</table>
</React.Fragment>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
</script>
</body>
We first create a React component called App
by extending the React.Component
class. Next, we add a contacts variable to the state
object which will be used to hold the contacts after we fetch them from the PHP REST endpoint using Axios.
Next, we define a React render()
method which returns a fragment that wraps the <h1>
header and <table>
elements.
In the table we loop through the this.state.contacts
and we display each <tr>
corresponding to each contact information.
Finally, we use the render() method of ReactDOM to actually mount our App
component to the DOM.
The contacts
array is empty. Let's use the Axios client to send a GET request to fetch data from /api/contacts.php
endpoint exposed by the PHP server.
In the App component add a componentDidMount()
life cycle method, which gets called when the component is mounted in the DOM, and inside it; add the code to fetch data:
componentDidMount() {
const url = '/api/contacts.php'
axios.get(url).then(response => response.data)
.then((data) => {
this.setState({ contacts: data })
console.log(this.state.contacts)
})
}
When data is fetched, we call the React setState method to update the state of the component with the fetched data.
Create a React Form for Submitting Data
Let's now add a React component that displays a form and handles submitting the form to the PHP backend. In your index.php
file add the following component before the App
component:
class ContactForm extends React.Component {
state = {
name: '',
email: '',
country: '',
city: '',
job: '',
}
handleFormSubmit( event ) {
event.preventDefault();
console.log(this.state);
}
render(){
return (
<form>
<label>Name</label>
<input type="text" name="name" value={this.state.name}
onChange={e => this.setState({ name: e.target.value })}/>
<label>Email</label>
<input type="email" name="email" value={this.state.email}
onChange={e => this.setState({ email: e.target.value })}/>
<label>Country</label>
<input type="text" name="country" value={this.state.country}
onChange={e => this.setState({ country: e.target.value })}/>
<label>City</label>
<input type="text" name="city" value={this.state.city}
onChange={e => this.setState({ city: e.target.value })}/>
<label>Job</label>
<input type="text" name="job" value={this.state.job}
onChange={e => this.setState({ job: e.target.value })}/>
<input type="submit" onClick={e => this.handleFormSubmit(e)} value="Create Contact" />
</form>);
}
}
Next include it in the App
component to be able to display it below the table:
class App extends React.Component {
// [...]
render() {
return (
<React.Fragment>
<!-- [...] -->
<ContactForm />
</React.Fragment>
);
}
}
Now let's change the handleFormSubmit()
of ContactForm
method to actually send the form data using Axios and FormData to our PHP REST endpoint which takes care of saving it in the MySQL database:
handleFormSubmit( event ) {
event.preventDefault();
let formData = new FormData();
formData.append('name', this.state.name)
formData.append('email', this.state.email)
formData.append('city', this.state.city)
formData.append('country', this.state.country)
formData.append('job', this.state.job)
axios({
method: 'post',
url: '/api/contacts.php',
data: formData,
config: { headers: {'Content-Type': 'multipart/form-data' }}
})
.then(function (response) {
//handle success
console.log(response)
})
.catch(function (response) {
//handle error
console.log(response)
});
}
Conclusion
In this tutorial, we've seen how to use PHP with MySQL, React and Axios to create a simple REST API CRUD example application. We have also seen how to handle forms in React and submit data to the server.
https://www.techiediaries.com/formdata/
In this post, we'll learn about the FormData interface available in modern web browsers as a part of the HTML5 spec.
We'll see examples of using FormData with Ajax, Angular 9, Ionic 5 and React.
What's FormData
FormData is simply a data structure that can be used to store key-value pairs. Just like its name suggests it's designed for holding forms data i.e you can use it with JavaScript to build an object that corresponds to an HTML form. It's mostly useful when you need to send form data to RESTful API endpoints, for example to upload single or multiple files using the XMLHttpRequest
interface, the fetch()
API or Axios.
You can create a FormData object by instantiating the FormData interface using the new
operator as follows:
const formData = new FormData()
The formData
reference refers to an instance of FormData. You can call many methods on the object to add and work with pairs of data. Each pair has a key and value.
These are the available methods on FormData objects:
append()
: used to append a key-value pair to the object. If the key already exists, the value is appended to the original value for that key,delete()
: used to deletes a key-value pair,entries()
: returns an Iterator object that you can use to loop through the list the key value pairs in the object,get()
: used to return the value for a key. If multiple values are appended, it returns the first value,getAll()
: used to return all the values for a specified key,has()
: used to check if there’s a key,keys()
: returns an Iterator object which you can use to list the available keys in the object,set()
: used to add a value to the object, with the specified key. This is going to relace the value if a key already exists,values()
: returns an Iterator object for the values of the FormData object.
File Upload Example with Vanilla JavaScript
Let's now see a simple example of file upload using vanilla JavaScript, XMLHttpRequest
and FormData
.
Navigate to your working folder and create and index.html
file with the following content:
<!DOCTYPE html>
<html>
<head>
<title>Parcel Sandbox</title>
<meta charset="UTF-8" />
</head>
<body>
<div id="app"></div>
<script src="index.js">
</script>
</body>
</html>
We simply create an HTML document with a <div>
identified by the app
ID. Next, we include the index.js
file using a <script>
tag.
Next, create the index.js
file and add following code:
document.getElementById("app").innerHTML = `
<h3>File Upload & FormData Example</h3>
<div>
<input type="file" id="fileInput" />
</div>
`;
const fileInput = document.querySelector("#fileInput");
const uploadFile = file => {
console.log("Uploading file...");
const API_ENDPOINT = "https://file.io";
const request = new XMLHttpRequest();
const formData = new FormData();
request.open("POST", API_ENDPOINT, true);
request.onreadystatechange = () => {
if (request.readyState === 4 && request.status === 200) {
console.log(request.responseText);
}
};
formData.append("file", file);
request.send(formData);
};
fileInput.addEventListener("change", event => {
const files = event.target.files;
uploadFile(files[0]);
});
We first insert an <input type="file" id="fileInput" />
element in our HTML page. This will be used to select the file that we'll be uploading.
Next, we query for the file input element using the querySelector()
method.
Next, we define the uploadFile()
method in which we first declare an API_ENDPOINT
variable that holds the address of our file uploading endpoint. Next, we create an XMLHttpRequest
request and an empty FormData
object.
We use the append method of FormData to append the file, passed as a parameter to the uploadFile()
method, to the file
key. This will create a key-value pair with file
as a key and the content of the passed file as a value.
Next, we send the request using the send()
method of XMLHttpRequest
and we pass in the FormData
object as an argument.
After defining the uploadFile()
method, we listen for the change event on the <input>
element and we call the uploadFile()
method with the selected file as an argument. The file is accessed from event.target.files
array.
You can experiment with this example from this code sandbox:
Uploading Multiple Files
You can easily modify the code above to support multiple file uploading.
First, you need to add the multiple
property to the <input>
element:
<input type="file" id="fileInput" multiple />
Now, you'll be able to select multiple files from your drive.
Next, change the uploadFile()
method to accept an array of files as an argument and simply loop through the array and append the files to the FormData
object:
const uploadFile = (files) => {
console.log("Uploading file...");
const API_ENDPOINT = "https://file.io";
const request = new XMLHttpRequest();
const formData = new FormData();
request.open("POST", API_ENDPOINT, true);
request.onreadystatechange = () => {
if (request.readyState === 4 && request.status === 200) {
console.log(request.responseText);
}
};
for (let i = 0; i < files.length; i++) {
formData.append(files[i].name, files[i])
}
request.send(formData);
};
Finally, call the method with an array of files as argument:
fileInput.addEventListener("change", event => {
const files = event.target.files;
uploadFile(files);
});
Next, you can check out these advanced tutorials for how to use FormData
with Angular, Ionic and React: