Forms in React Interviews

Guide to building interactive React forms, covering controlled vs uncontrolled components, diverse input types, complex state management, and robust error handling and validation strategies

Author
Ex-Meta Staff Engineer

Forms in React are a crucial part of building interactive applications, allowing users to input and submit data. In React, it is common to control form elements using state, making them dynamic yet predictable.

Controlled vs uncontrolled form components

In React, form inputs can be managed in two ways: controlled and uncontrolled. The main difference lies in how the form data is handled.

Controlled form components

In a controlled form component, React manages the form element's state. The value of the input is stored in a state variable and updated via an onChange handler.

import { useState } from 'react';
function ControlledForm() {
const [name, setName] = useState('');
function handleChange(event) {
setName(event.target.value);
}
function handleSubmit(event) {
event.preventDefault();
alert(`Submitted Name: ${name}`);
}
return (
<form onSubmit={handleSubmit}>
<label>
Name:
<input type="text" value={name} onChange={handleChange} />
</label>
<button type="submit">Submit</button>
</form>
);
}

In controlled components, the input value is stored in React state (name). The onChange handler updates the state as the user types. This ensures the value is always controlled by React.

Uncontrolled form components

In an uncontrolled form component, the form element's value is managed by the DOM itself rather than React state.

import { useRef } from 'react';
function UncontrolledForm() {
const nameRef = useRef();
function handleSubmit(event) {
event.preventDefault();
// Access form values using `FormData`
const formData = new FormData(event.target);
console.log('Name:', formData.get('name'));
// Alternatively, access the <input> via a ref
console.log('Name:', nameRef.current.value);
}
return (
<form onSubmit={handleSubmit}>
<label>
Name:
<input type="text" name="name" ref={nameRef} />
</label>
<button type="submit">Submit</button>
</form>
);
}

In uncontrolled components, the input value is not stored in React state. To access the form data upon submission, we can either:

  1. Use FormData: Access the form element directly from event.target, create a FormData instance from the form, and retrieve values using formData.get('name) (corresponds to the name attribute on <input>)
  2. Use refs: Use useRef() to reference the <input> field directly. The input value can be accessed via nameRef.current.value

When to use which?

FeatureControlled formUncontrolled form
Where state is storedReact state (useState)Native DOM
PerformanceRe-rendering needed on update, might cause issues in large formsMore performant for simple use cases
ValidationEasy to implementRequires manual validation
Form resetEasy (setState(""))Needs ref.current.value = "" or trigger a 'reset' event
Use caseDynamic forms, validation, real-time updatesSimple forms, file uploads, or integrating non-React code
  • Use controlled form components when you need to validate, manipulate, or track user input dynamically (e.g. toggling visibility of certain form fields based on previous responses) or nested form state
  • Use uncontrolled form components when working with large forms, integrating with non-React code, or optimizing performance

In interviews, considering the forms usually do not have many fields, both controlled components and uncontrolled approaches are viable. Personally, we'd lean towards controlled components since you might be asked to reset the values, add validation-on-typing, etc and it'd be easier to implement.

Handling different input types

The following are code examples for various input types using the controlled approach, meaning their values are stored in state and updated via onChange handlers.

Text input

A controlled text input updates its value in state as the user types.

import { useState } from 'react';
function TextInputExample() {
const [text, setText] = useState('');
return (
<div>
<label htmlFor="name-input">Name</label>
<input
id="name-input"
type="text"
value={text}
onChange={(event) => setText(event.target.value)}
/>
<p>Entered Text: {text}</p>
</div>
);
}
  • The input field's value is controlled by React state (text)
  • The onChange event updates state with setText(event.target.value)
  • This ensures React manages the input value dynamically

There are many other text-based values for the type attribute, each with various purposes:

typePurposeBuilt-in validation
textGeneral text inputNo
numberNumerical text inputYes, only numbers allowed. Additional validation via min/max attributes
emailEmail addressesYes, must contain @
passwordSecure password inputNo, but input is masked
searchSearch field with a clear buttonNo
telTelephone numbersNo. Additional validation via pattern attribute
urlURLsYes, must start with http:// or https://
datetime-localDate and time selectionYes
colorColor pickerYes

Recommendations:

  • Use text for generic input fields
  • Use number when you expect only numeric values and built-in validation for numbers
  • Use email, url, and tel to take advantage of built-in validation
  • Use password for secure text entry
  • Use search for fields optimized for searching
  • Use datetime-local when date & time selection is needed
  • Use color for color selection

Checkbox input

A checkbox is a boolean value (checked or unchecked).

import { useState } from 'react';
function CheckboxExample() {
const [isChecked, setIsChecked] = useState(false);
return (
<div>
<input
id="checkbox-input"
type="checkbox"
checked={isChecked}
onChange={(event) => setIsChecked(event.target.checked)}
/>
<label htmlFor="checkbox-input">Agree to terms and conditions</label>
<p>Checkbox is {isChecked ? 'checked' : 'unchecked'}</p>
</div>
);
}
  • The checked attribute is bound to isChecked state
  • The onChange event updates state with setIsChecked(event.target.checked)
  • This allows React to track the checkbox's checked status

Radio group

Radio buttons are used when selecting one option from multiple choices.

import { useState } from 'react';
function RadioGroupExample() {
const [gender, setGender] = useState('');
return (
<div>
<div>
<input
id="radio-male"
type="radio"
name="gender"
value="male"
checked={gender === 'male'}
onChange={(event) => setGender(event.target.value)}
/>
<label htmlFor="radio-male">Male</label>
</div>
<div>
<input
id="radio-female"
type="radio"
name="gender"
value="female"
checked={gender === 'female'}
onChange={(event) => setGender(event.target.value)}
/>
<label htmlFor="radio-female">Female</label>
</div>
<p>Selected gender: {gender}</p>
</div>
);
}
  • The name="gender" ensures only one option can be selected
  • The checked attribute checks if the current value matches the state (gender === "male" or "female")
  • The onChange updates state with the selected value

Textarea

A textarea is used for multi-line text input.

import { useState } from 'react';
function TextAreaExample() {
const [bio, setBio] = useState('');
return (
<div>
<label htmlFor="bio-input">Bio:</label>
<textarea
id="bio-input"
value={bio}
onChange={(event) => setBio(event.target.value)}
/>
<p>Bio Preview: {bio}</p>
</div>
);
}
  • Unlike HTML, React uses value instead of setting text inside <textarea>
  • The onChange updates the state with setBio(event.target.value), ensuring React controls the input

Select dropdown

A <select> dropdown allows users to choose one option.

import { useState } from 'react';
function SelectExample() {
const [fruit, setFruit] = useState('apple');
return (
<div>
<label htmlFor="favorite-fruit">Favorite fruit</label>
<select
id="favorite-fruit"
value={fruit}
onChange={(event) => setFruit(event.target.value)}>
<option value="apple">Apple</option>
<option value="banana">Banana</option>
<option value="orange">Orange</option>
</select>
<p>Selected fruit: {fruit}</p>
</div>
);
}
  • The value attribute is bound to the state (fruit)
  • The onChange updates the state when the user selects an option
  • This ensures the dropdown value is controlled by React

Summary

Input typeKey elementKey value attributeState update
Text input<input>valuesetState(event.target.value)
Checkbox input<input>checkedsetState(event.target.checked)
Radio group<input>checkedsetState(event.target.value)
Textarea<textarea>valuesetState(event.target.value)
Select dropdown<select>valuesetState(event.target.value)

Handling complex form state

When working with complex forms in React, managing state efficiently is crucial to ensure good performance and maintainability. Forms become complex when they include multiple input fields, dynamic field additions, nested structures, or advanced validation. Below are some strategies to handle complex form state effectively.

Using useReducer for complex forms

For forms with multiple fields and state dependencies, useReducer provides a structured way to manage state updates.

import { useReducer } from 'react';
// Define reducer function
function formReducer(state, action) {
switch (action.type) {
case 'UPDATE_FIELD':
return { ...state, [action.field]: action.value };
case 'RESET':
return initialState;
default:
return state;
}
}
// Initial form state
const initialState = {
name: '',
email: '',
age: '',
};
function ComplexFormWithReducer() {
const [state, dispatch] = useReducer(formReducer, initialState);
function handleChange(event) {
dispatch({
type: 'UPDATE_FIELD',
field: event.target.name,
value: event.target.value,
});
}
function handleReset() {
return dispatch({ type: 'RESET' });
}
return (
<form>
<input
type="text"
name="name"
value={state.name}
onChange={handleChange}
placeholder="Name"
/>
<input
type="email"
name="email"
value={state.email}
onChange={handleChange}
placeholder="Email"
/>
<input
type="number"
name="age"
value={state.age}
onChange={handleChange}
placeholder="Age"
/>
<button type="button" onClick={handleReset}>
Reset
</button>
</form>
);
}
  • Keeps state updates predictable and centralized
  • Useful for forms with conditional logic and dependencies
  • Prevents unnecessary re-renders compared to useState

Handling dynamic fields

When a form allows users to add or remove fields dynamically (e.g., multiple email inputs), state should be an array

import { useState } from 'react';
function DynamicForm() {
const [fields, setFields] = useState([{ id: 1, value: '' }]);
function addField() {
setFields([...fields, { id: fields.length + 1, value: '' }]);
}
function removeField(id) {
setFields(fields.filter((field) => field.id !== id));
}
function handleChange(id, event) {
const newFields = fields.map((field) =>
field.id === id ? { ...field, value: event.target.value } : field,
);
setFields(newFields);
}
return (
<form>
{fields.map((field) => (
<div key={field.id}>
<input
type="text"
value={field.value}
onChange={(event) => handleChange(field.id, event)}
placeholder="Enter value"
/>
<button type="button" onClick={() => removeField(field.id)}>
Remove
</button>
</div>
))}
<button type="button" onClick={addField}>
Add Field
</button>
</form>
);
}
  • Useful for forms where users can add multiple entries (e.g., multiple email addresses)
  • Keeps UI flexible without unnecessary predefined fields

Using form libraries for complex forms

Instead of managing everything manually, libraries like React Hook Form and Formik (unmaintained) simplify complex form state handling.

import { useForm, useFieldArray } from 'react-hook-form';
function FormWithReactHookForm() {
const { register, handleSubmit, control } = useForm({
defaultValues: { emails: [{ value: '' }] },
});
const { fields, append, remove } = useFieldArray({ control, name: 'emails' });
function onSubmit(data) {
console.log(data);
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
{fields.map((field, index) => (
<div key={field.id}>
<input {...register(`emails.${index}.value`)} placeholder="Email" />
<button type="button" onClick={() => remove(index)}>
Remove
</button>
</div>
))}
<button type="button" onClick={() => append({ value: '' })}>
Add Email
</button>
<button type="submit">Submit</button>
</form>
);
}
  • Reduces re-renders by using refs instead of state
  • Easy validation with built-in support for validation libraries like Zod, Yup, and Joi
  • Handles dynamic fields efficiently with useFieldArray

Handling nested form structures

Sometimes, forms have nested objects (e.g., address with street, city, and zip code).

import { useState } from 'react';
function NestedForm() {
const [form, setForm] = useState({
name: '',
address: { street: '', city: '', zip: '' },
});
function handleChange(event) {
const { name, value } = event.target;
setForm((prev) => ({
...prev,
address: { ...prev.address, [name]: value },
}));
}
return (
<form>
<input
type="text"
name="name"
placeholder="Name"
value={form.name}
onChange={(event) => setForm({ ...form, name: event.target.value })}
/>
<input
type="text"
name="street"
placeholder="Street"
value={form.address.street}
onChange={handleChange}
/>
<input
type="text"
name="city"
placeholder="City"
value={form.address.city}
onChange={handleChange}
/>
<input
type="text"
name="zip"
placeholder="ZIP Code"
value={form.address.zip}
onChange={handleChange}
/>
</form>
);
}
  • Helps manage related data cleanly in a single object
  • Useful for address, profile, or nested form sections

Best practices for complex form state

  • Use useReducer for structured state updates
  • Use dynamic fields for flexible input handling
  • Leverage form libraries like React Hook Form or Formik to simplify validation and state management
  • Use nested objects when dealing with grouped form data

Error handling and validation strategies

Handling errors and validating user input is essential for creating a smooth user experience in React forms. Validation ensures that users enter the correct data before submission, preventing invalid entries and reducing backend errors. Below are various strategies to handle validation and errors efficiently.

Basic client-side validation with useState

For simple forms, you can use state-based validation to check user input before submission.

import { useState } from 'react';
function BasicValidationForm() {
const [email, setEmail] = useState('');
const [error, setError] = useState('');
function handleSubmit(event) {
event.preventDefault();
if (!email) {
setError('Email is required');
return;
}
setError(''); // Clear error if validation passes
alert(`Submitted: ${email}`);
}
return (
<form onSubmit={handleSubmit}>
<label>
Email:
<input
type="email"
value={email}
onChange={(event) => setEmail(event.target.value)}
/>
</label>
{error && <p style={{ color: 'red' }}>{error}</p>}
<button type="submit">Submit</button>
</form>
);
}
  • Uses local state to track errors
  • Displays error messages when validation fails
  • Simple but sufficient for basic validation needs

Validate input fields with regular expressions

For structured input fields like emails, phone numbers, or passwords, regex-based validation is useful.

function EmailValidationForm() {
const [email, setEmail] = useState('');
const [error, setError] = useState('');
const validateEmail = (email) => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
};
function handleSubmit(event) {
event.preventDefault();
if (!validateEmail(email)) {
setError('Please enter a valid email address');
return;
}
setError('');
alert(`Valid Email: ${email}`);
}
return (
<form onSubmit={handleSubmit}>
<label>
Email:
<input
type="email"
value={email}
onChange={(event) => setEmail(event.target.value)}
/>
</label>
{error && <p style={{ color: 'red' }}>{error}</p>}
<button type="submit">Submit</button>
</form>
);
}
  • Uses regex to check if an email follows a valid format
  • Provides real-time feedback to users

Browser built-in HTML5 validation

Modern browsers provide built-in validation for certain input types.

function HTML5ValidationForm() {
return (
<form>
<label>
Email:
<input type="email" required />
</label>
<br />
<label>
Password (Min 6 characters):
<input type="password" minLength="6" required />
</label>
<br />
<label>
Phone (Numbers only):
<input type="tel" pattern="[0-9]{10}" required />
</label>
<br />
<button type="submit">Submit</button>
</form>
);
}
  • required: Ensures the field is filled
  • minLength: Restricts input length
  • pattern: Uses regex directly in HTML
  • Works without JavaScript, improving performance

React Hook Form for efficient validation

For complex forms, *React Hook Form provides optimized validation with minimal re-renders.

import { useForm } from 'React Hook Form;
function HookFormValidation() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm();
const onSubmit = (data) => alert(JSON.stringify(data));
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label>
Email:
<input
type="email"
{...register('email', { required: 'Email is required' })}
/>
</label>
{errors.email && <p style={{ color: 'red' }}>{errors.email.message}</p>}
<label>
Password:
<input
type="password"
{...register('password', {
required: 'Password is required',
minLength: {
value: 6,
message: 'Password must be at least 6 characters',
},
})}
/>
</label>
{errors.password && (
<p style={{ color: 'red' }}>{errors.password.message}</p>
)}
<button type="submit">Submit</button>
</form>
);
}
  • Uses refs instead of state, minimizing re-renders
  • Better performance for large forms
  • Built-in error handling with formState.errors

Handle server-side validation

Even with client-side validation, backend validation is necessary. You need to know how to display error messages from API responses.

import { useState } from 'react';
function ServerErrorHandlingForm() {
const [email, setEmail] = useState('');
const [error, setError] = useState('');
async function handleSubmit(event) {
event.preventDefault();
try {
setError(null);
const response = await fetch('/api/validate-email', {
method: 'POST',
body: JSON.stringify({ email }),
headers: { 'Content-Type': 'application/json' },
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || 'Something went wrong');
}
alert('Submission successful!');
} catch (err) {
setError(err.message);
}
}
return (
<form onSubmit={handleSubmit}>
<label>
Email:
<input
type="email"
value={email}
onChange={(event) => setEmail(event.target.value)}
/>
</label>
{error && <p style={{ color: 'red' }}>{error}</p>}
<button type="submit">Submit</button>
</form>
);
}
  • Uses fetch() to validate email on the server side
  • Displays API error messages if validation fails
  • Prevents security risks (e.g., hackers bypassing client-side validation)

Best practices for interviews

  • For simple forms, both controlled and uncontrolled inputs are viable
  • Wrap in <form> and leverage the browser form submit events
  • Use HTML5 validation where possible to reduce re-renders for better performance
  • Display clear error messages that guide users on how to fix their inputs
  • Merely client-side validation is insufficient. Server-side validation is also required to prevent security loopholes
  • If 3rd party libraries are allowed (e.g. in take home assignments), React Hook Form is a great addition that provides many useful features helps you achieve better performance at the same time

What you need to know for interviews

  • Uncontrolled and controlled forms: What's the difference, how to use either, and when to use
  • Form controls: Be able to build forms that use the various input types
  • Complex forms: How to build complex forms and the best practices
  • Error handling and validation: How to validate forms and show errors using native browser approaches and React approaches

Practice questions

Coding: