Skip to main content

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.

  1. Delete the current code in main.js.
  2. 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 />);
  1. Refresh your browser
  2. Type some text in the input
  3. 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.
  4. Update handleChange as follows
  const handleChange = (event) => {
setValue(event.target.value
+ .toUpperCase()
);
};
  1. Refresh the browser
  2. Type some text in the input
  3. We can now more clearly see we are controlling the value by storing it in the component's state.
  4. Remove the toUpperCase() call
  5. 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);
};
  1. 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.

  1. 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)

  1. Create the file styles.css
  2. 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.

  1. Modify the code to set the value property
     <form onSubmit={handleSubmit}>
    <input
    + value="initial value"
    type="text" ref={inputRef} />
    <button>Submit</button>
    </form>
  2. Refresh the page
  3. 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`.
  4. 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>
  5. Refresh the page
  6. The warning goes away
  • Use defaultValue to initialize uncontrolled components
  • Use value to initialize controlled 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

  1. 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>
    • read the value from the input when you click the add button
  2. 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
  1. 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 />);

Reference