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
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 refconsole.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:
- Use
FormData
: Access the form element directly fromevent.target
, create aFormData
instance from the form, and retrieve values usingformData.get('name)
(corresponds to thename
attribute on<input>
) - Use refs: Use
useRef()
to reference the<input>
field directly. The input value can be accessed vianameRef.current.value
When to use which?
Feature | Controlled form | Uncontrolled form |
---|---|---|
Where state is stored | React state (useState ) | Native DOM |
Performance | Re-rendering needed on update, might cause issues in large forms | More performant for simple use cases |
Validation | Easy to implement | Requires manual validation |
Form reset | Easy (setState("") ) | Needs ref.current.value = "" or trigger a 'reset' event |
Use case | Dynamic forms, validation, real-time updates | Simple 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><inputid="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 withsetText(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:
type | Purpose | Built-in validation |
---|---|---|
text | General text input | No |
number | Numerical text input | Yes, only numbers allowed. Additional validation via min /max attributes |
email | Email addresses | Yes, must contain @ |
password | Secure password input | No, but input is masked |
search | Search field with a clear button | No |
tel | Telephone numbers | No. Additional validation via pattern attribute |
url | URLs | Yes, must start with http:// or https:// |
datetime-local | Date and time selection | Yes |
color | Color picker | Yes |
Recommendations:
- Use
text
for generic input fields - Use
number
when you expect only numeric values and built-in validation for numbers - Use
email
,url
, andtel
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><inputid="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 toisChecked
state - The
onChange
event updates state withsetIsChecked(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><inputid="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><inputid="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><textareaid="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 withsetBio(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><selectid="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 type | Key element | Key value attribute | State update |
---|---|---|---|
Text input | <input> | value | setState(event.target.value) |
Checkbox input | <input> | checked | setState(event.target.checked) |
Radio group | <input> | checked | setState(event.target.value) |
Textarea | <textarea> | value | setState(event.target.value) |
Select dropdown | <select> | value | setState(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 functionfunction formReducer(state, action) {switch (action.type) {case 'UPDATE_FIELD':return { ...state, [action.field]: action.value };case 'RESET':return initialState;default:return state;}}// Initial form stateconst 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><inputtype="text"name="name"value={state.name}onChange={handleChange}placeholder="Name"/><inputtype="email"name="email"value={state.email}onChange={handleChange}placeholder="Email"/><inputtype="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}><inputtype="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><inputtype="text"name="name"placeholder="Name"value={form.name}onChange={(event) => setForm({ ...form, name: event.target.value })}/><inputtype="text"name="street"placeholder="Street"value={form.address.street}onChange={handleChange}/><inputtype="text"name="city"placeholder="City"value={form.address.city}onChange={handleChange}/><inputtype="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 passesalert(`Submitted: ${email}`);}return (<form onSubmit={handleSubmit}><label>Email:<inputtype="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:<inputtype="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 filledminLength
: Restricts input lengthpattern
: 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:<inputtype="email"{...register('email', { required: 'Email is required' })}/></label>{errors.email && <p style={{ color: 'red' }}>{errors.email.message}</p>}<label>Password:<inputtype="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:<inputtype="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: