Events and Forms in React
Event Handling in React
React events are named using camelCase and you pass a function as the event handler rather than a string.
Basic Event Handling
function Button() {
// ✅ Correct - pass function reference
const handleClick = () => {
console.log('Button clicked!');
};
return <button onClick={handleClick}>Click me</button>;
}
// ❌ Wrong - immediately invokes the function
<button onClick={handleClick()}>Click me</button>
// ✅ Inline arrow function (use sparingly)
<button onClick={() => console.log('Clicked')}>Click me</button>
Event Object
function handleClick(event) {
event.preventDefault();
console.log(event.type); // 'click'
console.log(event.target); // DOM element
console.log(event.currentTarget); // Element with handler
}
<button onClick={handleClick}>Click me</button>;
Common Events
function EventExamples() {
return (
<div>
{/* Mouse events */}
<button onClick={handleClick}>Click</button>
<div onMouseEnter={handleHover} onMouseLeave={handleLeave}>
Hover me
</div>
{/* Form events */}
<input
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
/>
{/* Keyboard events */}
<input onKeyDown={handleKeyDown} onKeyUp={handleKeyUp} />
{/* Focus events */}
<input onFocus={handleFocus} onBlur={handleBlur} />
</div>
);
}
Passing Arguments to Event Handlers
function TodoList({ todos }) {
const handleDelete = id => {
console.log('Deleting todo:', id);
};
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.text}
{/* Method 1: Arrow function */}
<button onClick={() => handleDelete(todo.id)}>Delete</button>
{/* Method 2: bind */}
<button onClick={handleDelete.bind(null, todo.id)}>Delete</button>
</li>
))}
</ul>
);
}
TypeScript Event Types
function Input() {
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
console.log(event.target.value);
};
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
};
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
console.log('Button clicked');
};
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
console.log('Enter pressed');
}
};
return (
<form onSubmit={handleSubmit}>
<input onChange={handleChange} onKeyDown={handleKeyDown} />
<button onClick={handleClick}>Submit</button>
</form>
);
}
Form Handling
Controlled Components
The form element's value is controlled by React state.
function ControlledForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = e => {
e.preventDefault();
console.log({ email, password });
};
return (
<form onSubmit={handleSubmit}>
<input
type='email'
value={email}
onChange={e => setEmail(e.target.value)}
placeholder='Email'
/>
<input
type='password'
value={password}
onChange={e => setPassword(e.target.value)}
placeholder='Password'
/>
<button type='submit'>Submit</button>
</form>
);
}
Multiple Inputs
function RegistrationForm() {
const [formData, setFormData] = useState({
username: '',
email: '',
password: '',
age: '',
});
const handleChange = e => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value,
}));
};
const handleSubmit = e => {
e.preventDefault();
console.log(formData);
};
return (
<form onSubmit={handleSubmit}>
<input
name='username'
value={formData.username}
onChange={handleChange}
placeholder='Username'
/>
<input
name='email'
type='email'
value={formData.email}
onChange={handleChange}
placeholder='Email'
/>
<input
name='password'
type='password'
value={formData.password}
onChange={handleChange}
placeholder='Password'
/>
<input
name='age'
type='number'
value={formData.age}
onChange={handleChange}
placeholder='Age'
/>
<button type='submit'>Register</button>
</form>
);
}
TypeScript Form with Type Safety
interface FormData {
username: string;
email: string;
password: string;
age: number;
}
function RegistrationForm() {
const [formData, setFormData] = useState<FormData>({
username: '',
email: '',
password: '',
age: 0,
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: name === 'age' ? parseInt(value) || 0 : value,
}));
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log(formData);
};
return <form onSubmit={handleSubmit}>{/* Form inputs */}</form>;
}
Checkbox Handling
function CheckboxForm() {
const [agreed, setAgreed] = useState(false);
const [interests, setInterests] = useState({
sports: false,
music: false,
reading: false,
});
const handleCheckbox = e => {
const { name, checked } = e.target;
setInterests(prev => ({
...prev,
[name]: checked,
}));
};
return (
<form>
<label>
<input
type='checkbox'
checked={agreed}
onChange={e => setAgreed(e.target.checked)}
/>
I agree to terms and conditions
</label>
<div>
<label>
<input
type='checkbox'
name='sports'
checked={interests.sports}
onChange={handleCheckbox}
/>
Sports
</label>
<label>
<input
type='checkbox'
name='music'
checked={interests.music}
onChange={handleCheckbox}
/>
Music
</label>
<label>
<input
type='checkbox'
name='reading'
checked={interests.reading}
onChange={handleCheckbox}
/>
Reading
</label>
</div>
</form>
);
}
Radio Buttons
function RadioForm() {
const [selectedPlan, setSelectedPlan] = useState('basic');
return (
<form>
<label>
<input
type='radio'
value='basic'
checked={selectedPlan === 'basic'}
onChange={e => setSelectedPlan(e.target.value)}
/>
Basic Plan
</label>
<label>
<input
type='radio'
value='premium'
checked={selectedPlan === 'premium'}
onChange={e => setSelectedPlan(e.target.value)}
/>
Premium Plan
</label>
<label>
<input
type='radio'
value='enterprise'
checked={selectedPlan === 'enterprise'}
onChange={e => setSelectedPlan(e.target.value)}
/>
Enterprise Plan
</label>
</form>
);
}
Select Dropdown
function SelectForm() {
const [country, setCountry] = useState('');
const [languages, setLanguages] = useState([]);
// Single select
const handleCountryChange = e => {
setCountry(e.target.value);
};
// Multiple select
const handleLanguagesChange = e => {
const options = Array.from(e.target.selectedOptions);
const values = options.map(option => option.value);
setLanguages(values);
};
return (
<form>
<select value={country} onChange={handleCountryChange}>
<option value=''>Select a country</option>
<option value='us'>United States</option>
<option value='uk'>United Kingdom</option>
<option value='ca'>Canada</option>
</select>
<select multiple value={languages} onChange={handleLanguagesChange}>
<option value='js'>JavaScript</option>
<option value='py'>Python</option>
<option value='java'>Java</option>
<option value='cpp'>C++</option>
</select>
</form>
);
}
Textarea
function TextareaForm() {
const [message, setMessage] = useState('');
return (
<textarea
value={message}
onChange={e => setMessage(e.target.value)}
placeholder='Enter your message'
rows={5}
/>
);
}
Form Validation
Basic Validation
function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [errors, setErrors] = useState({});
const validate = () => {
const newErrors = {};
if (!email) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(email)) {
newErrors.email = 'Email is invalid';
}
if (!password) {
newErrors.password = 'Password is required';
} else if (password.length < 6) {
newErrors.password = 'Password must be at least 6 characters';
}
return newErrors;
};
const handleSubmit = e => {
e.preventDefault();
const validationErrors = validate();
if (Object.keys(validationErrors).length === 0) {
console.log('Form is valid', { email, password });
setErrors({});
} else {
setErrors(validationErrors);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<input
type='email'
value={email}
onChange={e => setEmail(e.target.value)}
placeholder='Email'
/>
{errors.email && <span className='error'>{errors.email}</span>}
</div>
<div>
<input
type='password'
value={password}
onChange={e => setPassword(e.target.value)}
placeholder='Password'
/>
{errors.password && <span className='error'>{errors.password}</span>}
</div>
<button type='submit'>Login</button>
</form>
);
}
Real-time Validation
function SignupForm() {
const [username, setUsername] = useState('');
const [usernameError, setUsernameError] = useState('');
const validateUsername = value => {
if (value.length < 3) {
setUsernameError('Username must be at least 3 characters');
} else if (!/^[a-zA-Z0-9_]+$/.test(value)) {
setUsernameError(
'Username can only contain letters, numbers, and underscores'
);
} else {
setUsernameError('');
}
};
const handleUsernameChange = e => {
const value = e.target.value;
setUsername(value);
validateUsername(value);
};
return (
<div>
<input
value={username}
onChange={handleUsernameChange}
placeholder='Username'
className={usernameError ? 'error' : ''}
/>
{usernameError && <span className='error'>{usernameError}</span>}
</div>
);
}
Custom Form Hook
function useForm(initialValues, validationRules) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const handleChange = e => {
const { name, value } = e.target;
setValues(prev => ({
...prev,
[name]: value,
}));
};
const handleBlur = e => {
const { name } = e.target;
setTouched(prev => ({
...prev,
[name]: true,
}));
validateField(name, values[name]);
};
const validateField = (name, value) => {
const rule = validationRules[name];
if (!rule) return;
let error = '';
if (rule.required && !value) {
error = `${name} is required`;
} else if (rule.minLength && value.length < rule.minLength) {
error = `${name} must be at least ${rule.minLength} characters`;
} else if (rule.pattern && !rule.pattern.test(value)) {
error = rule.message || `${name} is invalid`;
}
setErrors(prev => ({
...prev,
[name]: error,
}));
};
const validateAll = () => {
Object.keys(values).forEach(key => {
validateField(key, values[key]);
});
};
const handleSubmit = callback => e => {
e.preventDefault();
validateAll();
const hasErrors = Object.values(errors).some(error => error !== '');
if (!hasErrors) {
callback(values);
}
};
return {
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
};
}
// Usage
function RegistrationForm() {
const { values, errors, touched, handleChange, handleBlur, handleSubmit } =
useForm(
{ email: '', password: '' },
{
email: {
required: true,
pattern: /\S+@\S+\.\S+/,
message: 'Please enter a valid email',
},
password: {
required: true,
minLength: 8,
},
}
);
const onSubmit = data => {
console.log('Form submitted:', data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input
name='email'
value={values.email}
onChange={handleChange}
onBlur={handleBlur}
/>
{touched.email && errors.email && <span>{errors.email}</span>}
</div>
<div>
<input
name='password'
type='password'
value={values.password}
onChange={handleChange}
onBlur={handleBlur}
/>
{touched.password && errors.password && <span>{errors.password}</span>}
</div>
<button type='submit'>Register</button>
</form>
);
}
File Upload
function FileUpload() {
const [selectedFile, setSelectedFile] = useState(null);
const [preview, setPreview] = useState(null);
const handleFileChange = e => {
const file = e.target.files[0];
setSelectedFile(file);
// Create preview for images
if (file && file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onloadend = () => {
setPreview(reader.result);
};
reader.readAsDataURL(file);
}
};
const handleUpload = async () => {
if (!selectedFile) return;
const formData = new FormData();
formData.append('file', selectedFile);
try {
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
});
const data = await response.json();
console.log('Upload successful:', data);
} catch (error) {
console.error('Upload failed:', error);
}
};
return (
<div>
<input type='file' onChange={handleFileChange} accept='image/*' />
{preview && <img src={preview} alt='Preview' style={{ maxWidth: 200 }} />}
<button onClick={handleUpload} disabled={!selectedFile}>
Upload
</button>
</div>
);
}
Uncontrolled Components (refs)
import { useRef } from 'react';
function UncontrolledForm() {
const emailRef = useRef();
const passwordRef = useRef();
const handleSubmit = e => {
e.preventDefault();
console.log({
email: emailRef.current.value,
password: passwordRef.current.value,
});
};
return (
<form onSubmit={handleSubmit}>
<input ref={emailRef} type='email' placeholder='Email' />
<input ref={passwordRef} type='password' placeholder='Password' />
<button type='submit'>Login</button>
</form>
);
}
Best Practices
- Use Controlled Components - Better control and validation
- Validate on Blur - Don't validate on every keystroke
- Clear Error Messages - Tell users exactly what's wrong
- Disable Submit on Invalid - Prevent invalid submissions
- Use Type="submit" - For submit buttons
- Prevent Default - Always call
e.preventDefault()in form handlers - Use FormData - For complex forms or file uploads
- TypeScript - Type your event handlers
Next Steps
- Learn about Conditional Rendering
- Explore Lists and Keys