Skip to main content

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

  1. Use Controlled Components - Better control and validation
  2. Validate on Blur - Don't validate on every keystroke
  3. Clear Error Messages - Tell users exactly what's wrong
  4. Disable Submit on Invalid - Prevent invalid submissions
  5. Use Type="submit" - For submit buttons
  6. Prevent Default - Always call e.preventDefault() in form handlers
  7. Use FormData - For complex forms or file uploads
  8. TypeScript - Type your event handlers

Next Steps