Forms
Controlled Components
In HTML, form elements such as <input>
, <textarea>
, and <select>
typically maintain their own state and update it based on user input.
For example, if you type in a text input the value property of the element holds what you typed (controls it).
In React, mutable state is typically kept in the state property of components, and only updated with setState().
We can combine the two by making the React state be the “single source of truth”. Then the React component that renders a form also controls what happens in that form on subsequent user input. An input form element whose value is controlled by React in this way is called a controlled component.
Controlled Function Components
Below is an example of what a controlled component would like like in a function component.
- Delete the current code in
main.js
. - Add the following code to
main.js
function ExampleForm() {
const [value, setValue] = React.useState('');
const handleChange = (event) => {
setValue(event.target.value);
};
return (
<form>
<input type="text" value={value} onChange={handleChange} />
<pre>{value}</pre>
</form>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<ExampleForm />);
- Refresh your browser
- Type some text in the
input
- Notice that this text immediately shows in
state
because we have written set the value and onChange properties to read and write from the parent component's surrounding state. - Update
handleChange
as follows
const handleChange = (event) => {
setValue(event.target.value
+ .toUpperCase()
);
};
- Refresh the browser
- Type some text in the
input
- We can now more clearly see we are controlling the value by storing it in the component's state.
- Remove the
toUpperCase()
call - To further understand controlled components: Comment out the implementation of
handleChange
and notice that when you type in the input nothing happens.
const handleChange = (event) => {
- setValue(event.target.value);
};
- Uncomment the
handleChange
implementation and verify it is working again.
Controlled Class Components
Below is an example of what a controlled component would look like in a class component.
class ExampleForm extends React.Component {
state = {
value: '',
};
handleChange = (event) => {
this.setState({ value: event.target.value });
};
render() {
return (
<form>
<input
type="text"
value={this.state.value}
onChange={this.handleChange}
/>
<pre>{JSON.stringify(this.state)}</pre>
</form>
);
}
}
ReactDOM.createRoot(document.getElementById('root')).render(<ExampleForm />);
Submitting
Handling the submission of the form using the same concepts we learning previously in the events section.
- Modify the code to prevent the default browser behavior of submitting the form data to the server and instead log the form data to the
console
.
Function Component Example
function SigninForm() {
const [username, setUsername] = React.useState('');
const [password, setPassword] = React.useState('');
const handleSubmit = (event) => {
event.preventDefault();
console.log(username, password);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
name="username"
value={username}
onChange={(event) => setUsername(event.target.value)}
/>
<input
type="password"
name="password"
value={password}
onChange={(event) => setPassword(event.target.value)}
/>
<button type="submit">Sign In</button>
</form>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<SigninForm />);
Class Component Example
class LoginForm extends React.Component {
state = {
username: '',
password: '',
};
handleChange = (event) => {
const { name, value } = event.target;
this.setState({ [name]: value });
};
handleSubmit = (event) => {
event.preventDefault();
console.log(this.state);
};
render() {
return (
<form onSubmit={this.handleSubmit}>
<input
type="text"
name="username"
value={this.state.username}
onChange={this.handleChange}
/>
<input
type="password"
name="password"
value={this.state.password}
onChange={this.handleChange}
/>
<button type="submit">Sign In</button>
</form>
);
}
}
ReactDOM.createRoot(document.getElementById('root')).render(<LoginForm />);
Controlling other Types of HTML Form Elements
The following example of a contact us form demonstrates how controlling other HTML form fields such as: <select>
, textarea
, and <input type='checkbox'>
is very similar to how we work with an <input>
.
Function Component Example
function ContactUsForm() {
const [department, setDepartment] = React.useState('');
const [message, setMessage] = React.useState('');
const [agreedToTerms, setAgreedToTerms] = React.useState(false);
function handleSubmit(event) {
event.preventDefault();
console.log('submitting', stateToString());
}
function stateToString() {
return JSON.stringify(
{
department,
message,
agreedToTerms,
},
null,
' '
);
}
return (
<form onSubmit={handleSubmit}>
<select
name="department"
value={department}
onChange={(e) => setDepartment(e.target.value)}
>
<option value="">Select...</option>
<option value="hr">Human Resources</option>
<option value="pr">Public Relations</option>
<option value="support">Support</option>
</select>
<br />
<br />
<textarea
name="message"
value={message}
onChange={(e) => setMessage(e.target.value)}
cols="30"
rows="10"
/>
<br />
<input
type="checkbox"
name="agreedToTerms"
checked={agreedToTerms}
onChange={(e) => setAgreedToTerms(e.target.checked)}
/>
I agree to the terms and conditions.
<br />
<button>Send</button>
</form>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<ContactUsForm />);
Class Component Example
class ContactUsForm extends React.Component {
state = {
department: '',
message: '',
agreedToTerms: false,
};
handleChange = (event) => {
const { type, name, value, checked } = event.target;
const updatedValue = type === 'checkbox' ? checked : value;
this.setState({ [name]: updatedValue });
};
handleSubmit = (event) => {
event.preventDefault();
console.log(this.state);
};
render() {
return (
<form onSubmit={this.handleSubmit}>
<select
name="department"
value={this.state.department}
onChange={this.handleChange}
>
<option value="">Select...</option>
<option value="hr">Human Resources</option>
<option value="pr">Public Relations</option>
<option value="support">Support</option>
</select>
<br />
<br />
<textarea
name="message"
value={this.state.message}
onChange={this.handleChange}
cols="30"
rows="10"
/>
<br />
<input
type="checkbox"
name="agreedToTerms"
checked={this.state.agreedToTerms}
onChange={this.handleChange}
/>
I agree to the terms and conditions.
<br />
<button>Send</button>
<pre>{JSON.stringify(this.state, null, ' ')}</pre>
</form>
);
}
}
ReactDOM.createRoot(document.getElementById('root')).render(<ContactUsForm />);
Notice that although these HTML form fields set their value differently:
<textarea>value goes here<textarea>
- The option with
selected
is selected.<select name="department">
<option value="">Select...</option>
<option value="hr">Human Resources</option>
<option selected value="pr">Public Relations</option>
<option value="support">Support</option>
</select> <input type='checkbox' checked=checked>
These have all been standardized to be set using the value property when using React.
Validation
- Validation of forms is something you can do in plain vanilla JavaScript.
- Validating user input is not even discussed in the React documentation.
- There are no specific features in React for validating forms.
- React leaves this to job for external libraries.
- Historically, the most popular React form validation library is Formik.
- More recently, the React form validation library React Hook Form has caught up to Formik and will likely soon pass it in popularity. If you are starting a new project a would recommend this library as it has better accessibility support.
- Here is a discussion on reddit of whether a Form Library is Necessary in React
To help you decide whether a library would be helpful in your use case, it can be helpful to manually implement form validation at first in your React application.
Below is an example of some basic validation implemented in our Contact Us form.
Validation (with Function Component & Hooks)
- Create the file
styles.css
- Add the following styles:
styles.css
.alert {
padding: 20px;
background-color: #f44336;
color: white;
width: 50%;
}
index.html
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Demos</title>
+ <link rel="stylesheet" href="/styles.css" />
</head>
...
main.js
function ContactUsForm() {
const [department, setDepartment] = React.useState('');
const [message, setMessage] = React.useState('');
const [agreedToTerms, setAgreedToTerms] = React.useState(false);
const [departmentError, setDepartmentError] = React.useState(null);
const [messageError, setMessageError] = React.useState(null);
const [agreedToTermsError, setAgreedToTermsError] = React.useState(null);
const [departmentTouched, setDepartmentTouched] = React.useState(false);
const [messageTouched, setMessageTouched] = React.useState(false);
const [agreedToTermsTouched, setAgreedToTermsTouched] = React.useState(false);
function handleSubmit(event) {
event.preventDefault();
const isValid = !departmentError && !messageError && !agreedToTermsError;
if (!isValid) {
return;
}
console.log('submitting', { department, message, agreedToTerms });
}
React.useEffect(() => {
validate();
}, [department, message, agreedToTerms]);
function validate() {
setDepartmentError(null);
setMessageError(null);
setAgreedToTermsError(null);
if (department === '') {
setDepartmentError('Department is required.');
}
if (message === '') {
setMessageError('Message is required.');
}
if (agreedToTerms === false) {
setAgreedToTermsError('You must agree to the terms and conditions.');
}
}
function stateToString() {
return JSON.stringify(
{
values: { department, message, agreedToTerms },
errors: { departmentError, messageError, agreedToTermsError },
touched: { departmentTouched, messageTouched, agreedToTermsTouched },
},
null,
' '
);
}
return (
<form onSubmit={handleSubmit}>
<select
name="department"
value={department}
onChange={(e) => setDepartment(e.target.value)}
onBlur={(e) => setDepartmentTouched(true)}
>
<option value="">Select...</option>
<option value="hr">Human Resources</option>
<option value="pr">Public Relations</option>
<option value="support">Support</option>
</select>
<br />
{departmentError && departmentTouched && (
<p className="alert">{departmentError}</p>
)}
<br />
<textarea
name="message"
value={message}
onChange={(e) => setMessage(e.target.value)}
onBlur={(e) => setMessageTouched(true)}
cols="30"
rows="10"
/>
<br />
{messageError && messageTouched && (
<p className="alert">{messageError}</p>
)}
<input
type="checkbox"
name="agreedToTerms"
checked={agreedToTerms}
onChange={(e) => setAgreedToTerms(e.target.checked)}
onBlur={(e) => setAgreedToTermsTouched(true)}
/>
I agree to the terms and conditions.
<br />
{agreedToTermsError && agreedToTermsTouched && (
<p className="alert">{agreedToTermsError}</p>
)}
<button>Send</button>
<pre>{stateToString()}</pre>
</form>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<ContactUsForm />);
Some things to notice in the code above:
- Validation messages are themselves local component state.
- The validations are called when the form is submitted.
- The && operator is used to conditionally display the error messages.
- The && operator is ideal in this case since there is no else case.
Validation (in a Class Component)
main.js
class ContactUsForm extends React.Component {
state = {
department: '',
message: '',
agreedToTerms: false,
departmentValidationMessage: null,
messageValidationMessage: null,
agreedToTermsValidationMessage: null,
};
handleChange = (event) => {
const { type, name, value, checked } = event.target;
const updatedValue = type === 'checkbox' ? checked : value;
this.setState({ [name]: updatedValue });
};
handleBlur = (event) => {
this.validate();
};
handleSubmit = (event) => {
event.preventDefault();
this.validate();
if (!this.isValid()) {
return;
}
console.log(this.state);
};
isValid = () => {
const {
departmentValidationMessage,
messageValidationMessage,
agreedToTermsValidationMessage,
} = this.state;
return (
departmentValidationMessage === null &&
messageValidationMessage === null &&
agreedToTermsValidationMessage === null
);
};
validate() {
const { department, message, agreedToTerms } = this.state;
this.setState({
departmentValidationMessage: null,
messageValidationMessage: null,
agreedToTermsValidationMessage: null,
});
if (!department) {
this.setState({ departmentValidationMessage: 'Department is required.' });
}
if (!message) {
this.setState({ messageValidationMessage: 'A message is required.' });
}
if (agreedToTerms === false) {
this.setState({
agreedToTermsValidationMessage:
'You must agree to the terms and conditions.',
});
}
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<select
name="department"
value={this.state.department}
onChange={this.handleChange}
onBlur={this.handleBlur}
>
<option value="">Select...</option>
<option value="hr">Human Resources</option>
<option value="pr">Public Relations</option>
<option value="support">Support</option>
</select>
<br />
{this.state.departmentValidationMessage && (
<p className="alert">{this.state.departmentValidationMessage}</p>
)}
<br />
<textarea
name="message"
value={this.state.message}
onChange={this.handleChange}
onBlur={this.handleBlur}
cols="30"
rows="10"
/>
<br />
{this.state.messageValidationMessage && (
<p className="alert">{this.state.messageValidationMessage}</p>
)}
<input
type="checkbox"
name="agreedToTerms"
checked={this.state.agreedToTerms}
onChange={this.handleChange}
onBlur={this.handleBlur}
/>
I agree to the terms and conditions.
<br />
{this.state.agreedToTermsValidationMessage && (
<p className="alert">{this.state.agreedToTermsValidationMessage}</p>
)}
<button>Send</button>
<pre>{JSON.stringify(this.state, null, ' ')}</pre>
</form>
);
}
}
ReactDOM.createRoot(document.getElementById('root')).render(<ContactUsForm />);
Uncontrolled Components
In most cases, React recommends using controlled components
to implement forms. In a controlled component, form data is handled by a React component. The alternative is uncontrolled components
, where form data is handled by the DOM
itself.
Refs
When writing an uncontrolled component
you use a ref
to get form values from the DOM directly instead of writing an event handler for every state update.
Function Component Example
function ExampleForm() {
const inputRef = React.useRef();
const handleSubmit = (event) => {
event.preventDefault();
console.log(inputRef.current);
console.log(inputRef.current.value);
};
return (
<form onSubmit={handleSubmit}>
<input type="text" ref={inputRef} />
<button>Submit</button>
</form>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<ExampleForm />);
Setting defaultValue
Try initializing the value property on the input.
- Modify the code to set the
value
property<form onSubmit={handleSubmit}>
<input
+ value="initial value"
type="text" ref={inputRef} />
<button>Submit</button>
</form> - Refresh the page
- This warning is displayed and the input will be read-only:
Warning: Failed prop type: You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set either `onChange` or `readOnly`.
- As the warning explains change the code to use defaultValue
<form onSubmit={handleSubmit}>
<input
+ defaultValue="initial value"
type="text" ref={inputRef} />
<button>Submit</button>
</form> - Refresh the page
- The warning goes away
- Use
defaultValue
to initializeuncontrolled components
- Use
value
to initializecontrolled components
Class Component Example
class ExampleForm extends React.Component {
inputRef = React.createRef();
handleSubmit = (event) => {
event.preventDefault();
console.log(this.inputRef.current);
console.log(this.inputRef.current.value);
};
render() {
return (
<form onSubmit={this.handleSubmit}>
<input type="text" ref={this.inputRef} />
<button>Submit</button>
</form>
);
}
}
ReactDOM.createRoot(document.getElementById('root')).render(<ExampleForm />);
File Input Example
In HTML, an <input type="file">
lets the user choose one or more files from their device storage to be uploaded to a server or manipulated by JavaScript via the File API.
<input type="file" />
In React, an <input type="file" />
is always an uncontrolled component because its value can only be set by a user, and not programmatically.
You should use the File API to interact with the files. The following example shows how to create a ref to the DOM node to access file(s) in a submit handler:
Function Component Example
const { useRef } = React;
function FileInput() {
const fileInput = useRef();
function handleSubmit(event) {
event.preventDefault();
console.log(fileInput.current);
if (!fileInput) return;
alert(`Selected file - ${fileInput.current.files[0].name}`);
}
return (
<form onSubmit={handleSubmit}>
<label>
Upload file:
<input type="file" ref={fileInput} />
</label>
<br />
<button type="submit">Submit</button>
</form>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<FileInput />);
Class Component Example
class FileInput extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.fileInput = React.createRef();
}
handleSubmit(event) {
event.preventDefault();
alert(`Selected file - ${this.fileInput.current.files[0].name}`);
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Upload file:
<input type="file" ref={this.fileInput} />
</label>
<br />
<button type="submit">Submit</button>
</form>
);
}
}
ReactDOM.createRoot(document.getElementById('root')).render(<FileInput />);
See the reference links below for a more complete example of a file upload component in React.
Items App Demo (CRUD) (continued)
Below we continue to expand on the Items (CRUD) Demo to use forms and do additional component communication. See the requirements listed below as well as the solution code.
Requirements
- Implement the feature to add an item
- create a Form component
- Add a text input and button to it
- render the Form in the Container by adding it above the list
- Note: since render needs to return one parent element you will need to wrap
<Form>
and<List>
in an outer<div>
or<React.Fragment>
- Note: since render needs to return one parent element you will need to wrap
- read the value from the input when you click the add button
- Add a feature to update an item inline
- Add an edit button to each item in the list
- Display an input and a button inline in place of the item when they click edit
- Save the update back into state in the app component
- Add a cancel link and use it to cancel out of editing mode. See the finished solution code below:
Solution (using Function Components & Hooks)
At this point, we are not calling an API yet we are just working with in-memory data but we will get to that next.
styles.css
body,
button,
input,
textarea,
li {
font-family: 'Open Sans', sans-serif;
font-size: 1em;
}
li {
list-style: none;
border-bottom: 1px solid #ddd;
padding: 0.25rem;
}
span {
margin: 15px;
}
button {
margin: 10px;
padding: 5px 15px 5px 15px;
background: transparent;
}
form {
margin: 15px;
}
input {
border: 1px solid darkgray;
}
html {
font-size: large;
}
h2 {
font-size: x-large;
padding-bottom: 1rem;
}
h3 {
font-size: large;
padding-bottom: 1rem;
}
.alert {
padding: 20px;
background-color: #f44336;
color: white;
width: 50%;
}
index.html
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Demos</title>
+ <link rel="stylesheet" href="/styles.css" />
</head>
...
main.js
function ID() {
return '_' + Math.random().toString(36).substr(2, 9);
}
class Item {
constructor(id, name) {
this.id = id;
this.name = name;
}
}
const initialItems = [
new Item(ID(), 'First Item'),
new Item(ID(), 'Second Item'),
new Item(ID(), 'Third Item'),
];
function ListItem({ item, onEdit, onRemove }) {
return (
<p>
<span>{item.name}</span>
<button onClick={() => onEdit(item)}>Edit</button>
<button onClick={() => onRemove(item)}>Remove</button>
</p>
);
}
function List({ items, onRemove, onUpdate }) {
const [editingItem, setEditingItem] = React.useState(null);
const handleEdit = (item) => {
setEditingItem(item);
};
const handleCancel = () => {
setEditingItem(null);
};
return (
<ul>
{items.map((item) => (
<li key={item.id}>
{item === editingItem ? (
<Form item={item} onSubmit={onUpdate} onCancel={handleCancel} />
) : (
<ListItem item={item} onEdit={handleEdit} onRemove={onRemove} />
)}
</li>
))}
</ul>
);
}
function Form({ item, onSubmit, onCancel, buttonValue }) {
const [inputValue, setInputValue] = React.useState(item.name || '');
const handleChange = (event) => {
event.preventDefault();
setInputValue(event.target.value);
};
const handleFormSubmit = (event) => {
event.preventDefault();
const submittedItem = {
id: item ? item.id : ID(),
name: inputValue,
};
onSubmit(submittedItem);
setInputValue('');
};
const handleCancel = (event) => {
event.preventDefault();
onCancel();
};
return (
<form onSubmit={handleFormSubmit}>
<input value={inputValue} onChange={handleChange} />
<button>{buttonValue || 'Save'}</button>
{onCancel && (
<a href="#" onClick={handleCancel}>
cancel
</a>
)}
</form>
);
}
function Container() {
const [items, setItems] = React.useState([]);
React.useEffect(() => setItems(initialItems), []);
const addItem = (item) => {
setItems([...items, item]);
};
const updateItem = (updatedItem) => {
let updatedItems = items.map((item) => {
return item.id === updatedItem.id
? Object.assign({}, item, updatedItem)
: item;
});
return setItems(updatedItems);
};
const removeItem = (removeThisItem) => {
const filteredItems = items.filter((item) => item.id != removeThisItem.id);
setItems(filteredItems);
};
return (
<React.Fragment>
<Form item="" onSubmit={addItem} buttonValue="Add" />
<List items={items} onRemove={removeItem} onUpdate={updateItem} />
</React.Fragment>
);
}
function App() {
return (
<div>
<Container />
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
Solution (using Class Components)
styles.css
body,
button,
input,
textarea,
li {
font-family: 'Open Sans', sans-serif;
font-size: 1em;
}
li {
list-style: none;
border-bottom: 1px solid #ddd;
}
span {
margin: 15px;
}
button {
margin: 10px;
padding: 5px 15px 5px 15px;
background: transparent;
}
form {
margin: 15px;
}
index.html
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Demos</title>
+ <link rel="stylesheet" href="styles.css" />
</head>
...
main.js
function ID() {
return '_' + Math.random().toString(36).substr(2, 9);
}
class Item {
constructor(id, name) {
this.id = id;
this.name = name;
}
}
const initialItems = [
new Item(ID(), 'First Item'),
new Item(ID(), 'Second Item'),
new Item(ID(), 'Third Item'),
];
class ListItem extends React.Component {
render() {
const { item, onEdit, onRemove } = this.props;
return (
<p>
<span>{item.name}</span>
<button onClick={() => onEdit(item)}>Edit</button>
<button onClick={() => onRemove(item)}>Remove</button>
</p>
);
}
}
class List extends React.Component {
state = {
editingItem: null,
};
handleEditClick = (item) => {
this.setState({ editingItem: item });
};
handleCancel = (item) => {
this.setState({ editingItem: null });
};
render() {
const { items, onRemove, onUpdate } = this.props;
return (
<ul>
{items.map((item) => (
<li key={item.id}>
{item === this.state.editingItem ? (
<Form
item={item}
onSubmit={onUpdate}
onCancel={this.handleCancel}
/>
) : (
<ListItem
item={item}
onEdit={this.handleEditClick}
onRemove={onRemove}
/>
)}
</li>
))}
</ul>
);
}
}
class Form extends React.Component {
state = {
inputValue: this.props.item.name || '',
};
handleChange = (event) => {
event.preventDefault();
this.setState({ inputValue: event.target.value });
};
handleFormSubmit = (event) => {
event.preventDefault();
const item = {
id: this.props.item ? this.props.item.id : ID(),
name: this.state.inputValue,
};
this.props.onSubmit(item);
this.setState({ inputValue: '' });
};
handleCancel = (event) => {
event.preventDefault();
this.props.onCancel();
};
render() {
return (
<form onSubmit={this.handleFormSubmit}>
<input value={this.state.inputValue} onChange={this.handleChange} />
<button>{this.props.buttonValue || 'Save'}</button>
{this.props.onCancel && (
<a href="#" onClick={this.handleCancel}>
cancel
</a>
)}
</form>
);
}
}
class Container extends React.Component {
state = {
items: [],
};
componentDidMount() {
this.setState({ items: initialItems });
}
addItem = (item) => {
this.setState((state) => ({ items: [...state.items, item] }));
};
updateItem = (updatedItem) => {
this.setState((state) => {
let items = state.items.map((item) => {
return item.id === updatedItem.id
? Object.assign({}, item, updatedItem)
: item;
});
return { items };
});
};
removeItem = (removeThisItem) => {
this.setState((state) => {
const items = state.items.filter((item) => item.id != removeThisItem.id);
return { items };
});
};
render() {
return (
<React.Fragment>
<Form item="" onSubmit={this.addItem} buttonValue="Add" />
<List
items={this.state.items}
onRemove={this.removeItem}
onUpdate={this.updateItem}
/>
</React.Fragment>
);
}
}
class App extends React.Component {
render() {
return (
<div>
<Container />
</div>
);
}
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);