Handling User Events — onClick, onChange and Forms

State becomes useful when it responds to user actions. Every interactive element in the MERN Blog — search inputs, filter dropdowns, like buttons, form submissions, modal toggles — triggers an event that updates state and causes a re-render with the new UI. In this lesson you will learn how React handles events, how to build controlled form inputs where React owns the value, and how to put it all together to build the blog’s interactive search and filter bar.

React Event Handling Basics

// React event handlers use camelCase and receive a synthetic event object
function EventExamples() {
  // ── onClick ───────────────────────────────────────────────────────────────
  const handleClick = () => console.log('clicked!');
  const handleClickWithEvent = (e) => {
    e.preventDefault(); // prevent default browser action
    console.log(e.target); // the clicked element
  };

  // ── onChange ──────────────────────────────────────────────────────────────
  const handleChange = (e) => {
    console.log(e.target.value);   // current input value
    console.log(e.target.name);    // input name attribute
    console.log(e.target.checked); // checkbox checked state
  };

  return (
    <div>
      {/* Inline arrow function — creates new function each render */}
      <button onClick={() => console.log('clicked')}>Click</button>

      {/* Reference to named handler — preferred for complex logic */}
      <button onClick={handleClick}>Click</button>

      {/* Pass arguments via inline arrow */}
      <button onClick={() => handleDelete(post._id)}>Delete</button>

      <input onChange={handleChange} />
      <input type="checkbox" onChange={handleChange} />
    </div>
  );
}
Note: React uses synthetic events — wrapper objects that normalise browser differences. They behave like native DOM events and expose the same properties (e.target, e.preventDefault(), e.stopPropagation()). For most purposes you can treat them exactly like native events. React pools and reuses synthetic event objects for performance — if you need to access a synthetic event asynchronously (in a setTimeout), call e.persist() first, or destructure the values you need before the async call.
Tip: Use inline arrow functions (onClick={() => handleDelete(id)}) when you need to pass arguments to an event handler. Never write onClick={handleDelete(id)} without the arrow function — that calls handleDelete(id) immediately during render and passes its return value (probably undefined) as the click handler, causing an infinite loop or unexpected behaviour.
Warning: Never call event handler functions with parentheses directly in JSX: onClick={handleClick()}. The parentheses call the function during render, not when the user clicks. You want to pass a reference: onClick={handleClick} (no parentheses). The only exception is when you wrap it in an arrow function to pass arguments: onClick={() => handleDelete(id)}.

Controlled Components — Form Inputs

// In React, form inputs can be "controlled" — React owns the value via state
// The input value always reflects the state, and every keystroke updates the state

function SearchBar({ onSearch }) {
  const [query, setQuery] = useState('');

  const handleChange = (e) => {
    setQuery(e.target.value);
    onSearch(e.target.value); // notify parent on every keystroke
  };

  const handleClear = () => {
    setQuery('');
    onSearch('');
  };

  return (
    <div className="search-bar">
      <input
        type="text"
        value={query}          // ← controlled: value is always from state
        onChange={handleChange} // ← every keystroke updates state
        placeholder="Search posts..."
        aria-label="Search posts"
      />
      {query && (
        <button onClick={handleClear} aria-label="Clear search">×</button>
      )}
    </div>
  );
}

Building a Complete Form with useState

function LoginForm({ onSubmit }) {
  const [formData, setFormData] = useState({ email: '', password: '' });
  const [errors,   setErrors]   = useState({});
  const [loading,  setLoading]  = useState(false);

  // Generic change handler for all inputs
  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));
    // Clear field error when user starts typing
    if (errors[name]) setErrors(prev => ({ ...prev, [name]: '' }));
  };

  const validate = () => {
    const newErrors = {};
    if (!formData.email)               newErrors.email    = 'Email is required';
    if (!formData.email.includes('@'))  newErrors.email    = 'Invalid email format';
    if (!formData.password)            newErrors.password = 'Password is required';
    if (formData.password.length < 8)  newErrors.password = 'Minimum 8 characters';
    return newErrors;
  };

  const handleSubmit = async (e) => {
    e.preventDefault(); // prevent page reload

    const validationErrors = validate();
    if (Object.keys(validationErrors).length > 0) {
      setErrors(validationErrors);
      return;
    }

    setLoading(true);
    try {
      await onSubmit(formData);
    } catch (err) {
      setErrors({ general: err.message || 'Login failed' });
    } finally {
      setLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit} noValidate>
      {errors.general && <p className="error">{errors.general}</p>}

      <div className="form-group">
        <label htmlFor="email">Email</label>
        <input
          id="email"
          name="email"
          type="email"
          value={formData.email}
          onChange={handleChange}
          className={errors.email ? 'input--error' : ''}
        />
        {errors.email && <p className="field-error">{errors.email}</p>}
      </div>

      <div className="form-group">
        <label htmlFor="password">Password</label>
        <input
          id="password"
          name="password"
          type="password"
          value={formData.password}
          onChange={handleChange}
          className={errors.password ? 'input--error' : ''}
        />
        {errors.password && <p className="field-error">{errors.password}</p>}
      </div>

      <button type="submit" disabled={loading}>
        {loading ? 'Logging in...' : 'Log In'}
      </button>
    </form>
  );
}

Common Event Types in the MERN Blog

Event JSX Prop Use In Blog
Mouse click onClick Like button, delete, open modal, submit
Input change onChange Search query, form fields, tag input
Form submit onSubmit Login, register, create post forms
Key press onKeyDown Submit on Enter in search bar
Focus / blur onFocus / onBlur Show/hide input hints, validate on blur
Mouse enter/leave onMouseEnter / onMouseLeave Hover tooltips, card hover effects
Image load error onError Fallback avatar when image fails

Common Mistakes

Mistake 1 — Calling a handler with parentheses in JSX

❌ Wrong — calls the function immediately during render:

<button onClick={handleDelete()}>Delete</button>
// handleDelete() executes during render, its return value (undefined) becomes the handler

✅ Correct — pass a reference, not a call:

<button onClick={handleDelete}>Delete</button>          // ✓ no args needed
<button onClick={() => handleDelete(post._id)}>Delete</button> // ✓ with args

Mistake 2 — Uncontrolled input (no value prop)

❌ Wrong — input value not tied to state (uncontrolled):

<input onChange={e => setQuery(e.target.value)} />
// React does not control the value — cannot programmatically clear or set it

✅ Correct — always provide the value prop for controlled inputs:

<input value={query} onChange={e => setQuery(e.target.value)} /> // ✓

Mistake 3 — Not calling e.preventDefault() on form submit

❌ Wrong — the form causes a full page reload on submit:

const handleSubmit = (e) => {
  // Missing: e.preventDefault()
  loginUser(formData); // runs briefly, then page reloads and loses everything
};

✅ Correct — prevent default form submission:

const handleSubmit = (e) => {
  e.preventDefault(); // stops page reload ✓
  loginUser(formData);
};

Quick Reference

Task Code
Click handler onClick={handleClick}
Click with argument onClick={() => handleDelete(id)}
Input change onChange={e => setValue(e.target.value)}
Controlled input value={state} onChange={handler}
Form submit onSubmit={e => { e.preventDefault(); ... }}
Checkbox checked={bool} onChange={e => setVal(e.target.checked)}
Generic form handler setForm(prev => ({ ...prev, [e.target.name]: e.target.value }))

🧠 Test Yourself

You write <button onClick={handleDelete(post._id)}>. Every time the component renders, the posts start being deleted one by one even without clicking. Why?