Episode 2 — React Frontend Architecture NextJS / 2.5 — Event Handling and Conditional Rendering

2.5.a — Handling Click Events in React

In one sentence: React wraps browser events in a cross-browser SyntheticEvent system that lets you attach handlers declaratively in JSX, giving you a consistent API for clicks, keyboard input, mouse movement, and every other user interaction.

Navigation: ← Overview · Next → Controlled Inputs


Table of Contents

  1. How Events Work in the Browser (Quick Recap)
  2. React's Synthetic Event System
  3. Your First Click Handler
  4. The Event Object — What You Get
  5. Passing Arguments to Event Handlers
  6. Inline Handlers vs Named Handlers
  7. The "this" Problem (And Why It Doesn't Exist in Functional Components)
  8. Event Propagation — Bubbling and Capturing
  9. Stopping Propagation and Preventing Default
  10. Common Event Types in React
  11. Mouse Events Deep Dive
  12. Keyboard Events Deep Dive
  13. Focus and Blur Events
  14. Touch Events for Mobile
  15. Event Delegation in React — You Don't Need It
  16. TypeScript and Events
  17. Performance Considerations
  18. Common Mistakes and Anti-Patterns
  19. Real-World Examples
  20. Key Takeaways

1. How Events Work in the Browser (Quick Recap)

Before understanding React events, you need to know how the browser handles them natively.

The DOM Event Flow

When a user clicks a button, the browser doesn't just fire an event on that button. It follows a three-phase process:

                    Window
                      │
           ┌──────────┴──────────┐
           │     CAPTURING       │
           │   (top → target)    │
           ▼                     │
        Document                 │
           │                     │
         <html>                  │
           │                     │
          <body>                 │
           │                     │
          <div>                  │
           │                     │
  ┌────────▼─────────┐          │
  │     <button>     │ ← TARGET │
  │   (click here)   │          │
  └────────┬─────────┘          │
           │                     │
           │     BUBBLING        │
           │   (target → top)    │
           └─────────────────────┘

Three phases:

PhaseDirectionDescription
CaptureWindow → TargetEvent travels DOWN the tree
TargetAt the elementEvent fires on the clicked element
BubbleTarget → WindowEvent travels UP the tree

Native JavaScript Event Handling

// Native DOM — imperative, manual
const button = document.querySelector('#myButton');

button.addEventListener('click', function(event) {
  console.log('Clicked!', event.target);
});

// You also need to clean up:
button.removeEventListener('click', handler);

Problems with native events:

  • Manual cleanup required (memory leaks)
  • Cross-browser inconsistencies (IE vs Chrome vs Firefox)
  • Event delegation is manual and error-prone
  • No integration with component lifecycle

React solves ALL of these problems.


2. React's Synthetic Event System

React doesn't attach event listeners to individual DOM nodes. Instead, it uses a single event listener at the root of your app and delegates all events through its own system.

What is a SyntheticEvent?

A SyntheticEvent is React's cross-browser wrapper around the native browser event. It has the same interface as native events (stopPropagation(), preventDefault(), target, etc.) but works identically across all browsers.

┌─────────────────────────────────────────┐
│            React Root (#root)            │
│                                          │
│  Single event listener for ALL events    │
│                                          │
│  ┌─────────────────────────────────┐     │
│  │   User clicks <button>          │     │
│  │          │                       │     │
│  │          ▼                       │     │
│  │   Native event captured          │     │
│  │          │                       │     │
│  │          ▼                       │     │
│  │   SyntheticEvent created         │     │
│  │          │                       │     │
│  │          ▼                       │     │
│  │   React finds component          │     │
│  │          │                       │     │
│  │          ▼                       │     │
│  │   Calls your handler(event)      │     │
│  │          │                       │     │
│  │          ▼                       │     │
│  │   SyntheticEvent recycled        │     │
│  └─────────────────────────────────┘     │
└─────────────────────────────────────────┘

SyntheticEvent vs Native Event

FeatureNative EventSyntheticEvent
Cross-browserInconsistentConsistent API
CleanupManual removeEventListenerAutomatic on unmount
PerformanceOne listener per elementSingle root listener
Access native eventDirectevent.nativeEvent
Event poolingN/ARemoved in React 17+
Works with JSXNoYes

Accessing the Native Event

function HandleNative() {
  function handleClick(event) {
    // SyntheticEvent
    console.log(event);              // SyntheticEvent {...}
    console.log(event.type);         // "click"
    
    // Access the real browser event
    console.log(event.nativeEvent);  // MouseEvent {...}
    console.log(event.nativeEvent instanceof MouseEvent); // true
  }

  return <button onClick={handleClick}>Click me</button>;
}

3. Your First Click Handler

The Simplest Possible Handler

function ClickCounter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
  }

  return (
    <button onClick={handleClick}>
      Clicked {count} times
    </button>
  );
}

Key syntax rules:

// ✅ CORRECT: Pass a reference to the function
<button onClick={handleClick}>

// ❌ WRONG: Calling the function immediately
<button onClick={handleClick()}>
// This calls handleClick during RENDER, not on click!

// ✅ CORRECT: Arrow function wrapper (for passing arguments)
<button onClick={() => handleClick('hello')}>

// ❌ WRONG: String handler (this isn't HTML)
<button onClick="handleClick()">

React Event Names vs HTML

React uses camelCase for event names, not lowercase:

HTML AttributeReact Prop
onclickonClick
onchangeonChange
onmouseoveronMouseOver
onkeydownonKeyDown
onfocusonFocus
onsubmitonSubmit
ondragstartonDragStart
onscrollonScroll

4. The Event Object — What You Get

Every event handler receives a SyntheticEvent object as its first argument.

Common Properties

function EventInspector() {
  function handleClick(event) {
    // ─── Target Info ───
    console.log(event.target);         // The DOM element that was clicked
    console.log(event.currentTarget);  // The DOM element the handler is attached to
    
    // ─── Event Info ───
    console.log(event.type);           // "click"
    console.log(event.timeStamp);      // When the event occurred
    console.log(event.eventPhase);     // 3 (bubbling)
    
    // ─── Mouse Position ───
    console.log(event.clientX);        // X relative to viewport
    console.log(event.clientY);        // Y relative to viewport
    console.log(event.pageX);          // X relative to document
    console.log(event.pageY);          // Y relative to document
    console.log(event.screenX);        // X relative to screen
    console.log(event.screenY);        // Y relative to screen
    
    // ─── Modifier Keys ───
    console.log(event.altKey);         // Was Alt held?
    console.log(event.ctrlKey);        // Was Ctrl held?
    console.log(event.shiftKey);       // Was Shift held?
    console.log(event.metaKey);        // Was Cmd/Win held?
    
    // ─── Mouse Button ───
    console.log(event.button);         // 0=left, 1=middle, 2=right
    
    // ─── Methods ───
    event.preventDefault();            // Cancel default browser action
    event.stopPropagation();           // Stop event from bubbling
  }

  return <button onClick={handleClick}>Inspect Event</button>;
}

target vs currentTarget

This is one of the most confusing aspects for beginners:

function TargetDemo() {
  function handleClick(event) {
    console.log('target:', event.target.tagName);
    console.log('currentTarget:', event.currentTarget.tagName);
  }

  return (
    <div onClick={handleClick} style={{ padding: 20, background: '#eee' }}>
      {/* Click the span: target = SPAN, currentTarget = DIV */}
      {/* Click the div padding: target = DIV, currentTarget = DIV */}
      <span>Click me or click the padding</span>
    </div>
  );
}
┌──────────────── <div onClick={handleClick}> ────────────────┐
│                                                              │
│  currentTarget is ALWAYS the div (where handler is)          │
│                                                              │
│  ┌──────────────── <span> ──────────────────┐               │
│  │                                           │               │
│  │  If you click HERE, target = span         │               │
│  │                                           │               │
│  └───────────────────────────────────────────┘               │
│                                                              │
│  If you click HERE (padding), target = div                   │
│                                                              │
└──────────────────────────────────────────────────────────────┘
event.targetevent.currentTarget
DefinitionElement that was actually clickedElement the handler is on
Changes?Yes — depends on where user clickedNo — always the handler element
Use forFiguring out WHAT was clickedAccessing the element with the handler

5. Passing Arguments to Event Handlers

Method 1: Arrow Function Wrapper

function ItemList() {
  const items = ['Apple', 'Banana', 'Cherry'];

  function handleDelete(itemName) {
    console.log(`Deleting ${itemName}`);
  }

  return (
    <ul>
      {items.map(item => (
        <li key={item}>
          {item}
          {/* Arrow wrapper — creates new function each render */}
          <button onClick={() => handleDelete(item)}>Delete</button>
        </li>
      ))}
    </ul>
  );
}

Method 2: Currying (Function That Returns a Function)

function ItemList() {
  const items = ['Apple', 'Banana', 'Cherry'];

  // Curried handler
  function handleDelete(itemName) {
    return function (event) {
      console.log(`Deleting ${itemName}`, event);
    };
  }

  // Shorter with arrow functions
  const handleDelete2 = (itemName) => (event) => {
    console.log(`Deleting ${itemName}`, event);
  };

  return (
    <ul>
      {items.map(item => (
        <li key={item}>
          {item}
          <button onClick={handleDelete(item)}>Delete</button>
        </li>
      ))}
    </ul>
  );
}

Method 3: Data Attributes (No Extra Functions)

function ItemList() {
  const items = ['Apple', 'Banana', 'Cherry'];

  function handleDelete(event) {
    const itemName = event.currentTarget.dataset.item;
    console.log(`Deleting ${itemName}`);
  }

  return (
    <ul>
      {items.map(item => (
        <li key={item}>
          {item}
          <button data-item={item} onClick={handleDelete}>Delete</button>
        </li>
      ))}
    </ul>
  );
}

When to Use Which

MethodCreates FunctionsExtra Re-renders?Best For
Arrow wrapperYes, every renderWith React.memo yesSmall lists, simple handlers
CurryingYes, every renderSame as arrowWhen you need event + args
Data attributesNoNoLarge lists, performance-critical

6. Inline Handlers vs Named Handlers

Inline Handler

function InlineExample() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}

Named Handler

function NamedExample() {
  const [count, setCount] = useState(0);

  function handleIncrement() {
    setCount(prev => prev + 1);
  }

  return (
    <button onClick={handleIncrement}>
      Count: {count}
    </button>
  );
}

Comparison

AspectInlineNamed
ReadabilityGood for 1-linersBetter for complex logic
DebuggingAnonymous in stack tracesNamed in stack traces
ReusabilityCan't reuseCan use in multiple JSX elements
TestingHarder to testCan export and test separately
ConventionsOK for simple settersPreferred for anything complex

Naming Convention

React has a strong convention for handler names:

// Pattern: handle + EventType or handle + What + EventType
function SearchForm() {
  function handleSubmit(event) { /* ... */ }         // Form event
  function handleSearchChange(event) { /* ... */ }   // Specific input
  function handleReset() { /* ... */ }               // Action
  function handleFilterToggle() { /* ... */ }        // Toggle action

  // For props: on + EventType
  return (
    <ChildComponent
      onSubmit={handleSubmit}
      onSearchChange={handleSearchChange}
      onReset={handleReset}
    />
  );
}

Rule of thumb:

  • Inside component: handle + Noun + Verb → handleFormSubmit
  • Props to children: on + Noun + Verb → onFormSubmit

7. The "this" Problem (And Why It Doesn't Exist in Functional Components)

In class components, this was a constant headache:

// ❌ CLASS COMPONENT — "this" problem
class BrokenButton extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
    // Must bind "this" or it's undefined in the handler!
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    // Without binding, "this" is undefined here
    this.setState({ count: this.state.count + 1 });
  }

  render() {
    return <button onClick={this.handleClick}>Count: {this.state.count}</button>;
  }
}
// ✅ FUNCTIONAL COMPONENT — no "this" at all
function WorkingButton() {
  const [count, setCount] = useState(0);

  function handleClick() {
    // No "this" — just use the variable directly
    setCount(count + 1);
  }

  return <button onClick={handleClick}>Count: {count}</button>;
}

Why functional components don't have this problem:

  • No class instance → no this
  • State comes from useState → captured by closure
  • Handlers are regular functions → this doesn't matter
  • Arrow functions in function bodies → always have correct scope

This is one of the major reasons React moved toward functional components.


8. Event Propagation — Bubbling and Capturing

Bubbling (Default Behavior)

Events bubble UP from the target to the root:

function BubblingDemo() {
  return (
    <div onClick={() => console.log('3. DIV clicked')}>
      <ul onClick={() => console.log('2. UL clicked')}>
        <li onClick={() => console.log('1. LI clicked')}>
          Click me
        </li>
      </ul>
    </div>
  );
}

// Clicking the <li> logs:
// 1. LI clicked
// 2. UL clicked
// 3. DIV clicked

Capturing

To listen during the CAPTURE phase (top → down), append Capture to the event name:

function CapturingDemo() {
  return (
    <div onClickCapture={() => console.log('1. DIV capture')}>
      <ul onClickCapture={() => console.log('2. UL capture')}>
        <li onClick={() => console.log('3. LI bubble')}>
          Click me
        </li>
      </ul>
    </div>
  );
}

// Clicking the <li> logs:
// 1. DIV capture    (top-down)
// 2. UL capture     (top-down)
// 3. LI bubble      (bottom-up)

Full Order with Both Phases

function FullEventFlow() {
  return (
    <div
      onClickCapture={() => console.log('1. outer CAPTURE')}
      onClick={() => console.log('6. outer BUBBLE')}
    >
      <div
        onClickCapture={() => console.log('2. middle CAPTURE')}
        onClick={() => console.log('5. middle BUBBLE')}
      >
        <button
          onClickCapture={() => console.log('3. target CAPTURE')}
          onClick={() => console.log('4. target BUBBLE')}
        >
          Click
        </button>
      </div>
    </div>
  );
}

// Output order: 1, 2, 3, 4, 5, 6

9. Stopping Propagation and Preventing Default

stopPropagation — Stop Event from Reaching Parents

function StopPropagationDemo() {
  function handleParentClick() {
    console.log('Parent clicked — this should NOT fire');
  }

  function handleButtonClick(event) {
    event.stopPropagation(); // Prevents bubbling to parent
    console.log('Button clicked — only this fires');
  }

  return (
    <div onClick={handleParentClick} style={{ padding: 40, background: '#eee' }}>
      <button onClick={handleButtonClick}>Click Me</button>
    </div>
  );
}

preventDefault — Cancel Browser Default Action

function PreventDefaultDemo() {
  // ─── Prevent form from reloading the page ───
  function handleSubmit(event) {
    event.preventDefault(); // No page reload
    console.log('Form submitted via JavaScript');
  }

  // ─── Prevent link from navigating ───
  function handleLinkClick(event) {
    event.preventDefault(); // No navigation
    console.log('Link clicked but not navigating');
  }

  // ─── Prevent context menu ───
  function handleContextMenu(event) {
    event.preventDefault(); // No right-click menu
    console.log('Custom context menu instead');
  }

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input type="text" />
        <button type="submit">Submit</button>
      </form>

      <a href="https://google.com" onClick={handleLinkClick}>
        Google (won't navigate)
      </a>

      <div onContextMenu={handleContextMenu}>
        Right-click here for custom menu
      </div>
    </div>
  );
}

stopPropagation vs preventDefault

MethodWhat it doesUse case
stopPropagation()Stops event from reaching parent elementsButton inside clickable card
preventDefault()Cancels browser's default actionForm submit, link click, drag
Both togetherStops both bubbling AND defaultComplex nested interactive elements

Real-World: Button Inside a Clickable Card

function CardWithButton({ title, onCardClick, onDelete }) {
  function handleDeleteClick(event) {
    event.stopPropagation(); // Don't trigger card click
    onDelete();
  }

  return (
    <div onClick={onCardClick} className="card" style={{ cursor: 'pointer' }}>
      <h3>{title}</h3>
      <button onClick={handleDeleteClick}>🗑️ Delete</button>
    </div>
  );
}

10. Common Event Types in React

Complete Event Category Reference

┌─────────────────────────────────────────────────────────┐
│                 React Event Categories                    │
├──────────────┬──────────────────────────────────────────┤
│ Category     │ Events                                    │
├──────────────┼──────────────────────────────────────────┤
│ Mouse        │ onClick, onDoubleClick, onMouseDown,      │
│              │ onMouseUp, onMouseMove, onMouseEnter,      │
│              │ onMouseLeave, onMouseOver, onMouseOut       │
├──────────────┼──────────────────────────────────────────┤
│ Keyboard     │ onKeyDown, onKeyUp, onKeyPress (deprecated)│
├──────────────┼──────────────────────────────────────────┤
│ Form         │ onChange, onInput, onSubmit, onReset,      │
│              │ onInvalid                                  │
├──────────────┼──────────────────────────────────────────┤
│ Focus        │ onFocus, onBlur, onFocusCapture,           │
│              │ onBlurCapture                              │
├──────────────┼──────────────────────────────────────────┤
│ Touch        │ onTouchStart, onTouchMove, onTouchEnd,     │
│              │ onTouchCancel                              │
├──────────────┼──────────────────────────────────────────┤
│ Clipboard    │ onCopy, onCut, onPaste                    │
├──────────────┼──────────────────────────────────────────┤
│ Drag         │ onDrag, onDragStart, onDragEnd,            │
│              │ onDragEnter, onDragLeave, onDragOver,      │
│              │ onDrop                                     │
├──────────────┼──────────────────────────────────────────┤
│ Scroll       │ onScroll                                   │
├──────────────┼──────────────────────────────────────────┤
│ Wheel        │ onWheel                                    │
├──────────────┼──────────────────────────────────────────┤
│ Animation    │ onAnimationStart, onAnimationEnd,          │
│              │ onAnimationIteration                       │
├──────────────┼──────────────────────────────────────────┤
│ Transition   │ onTransitionEnd                            │
├──────────────┼──────────────────────────────────────────┤
│ Media        │ onPlay, onPause, onEnded, onLoadedData,    │
│              │ onTimeUpdate, onVolumeChange               │
├──────────────┼──────────────────────────────────────────┤
│ Image        │ onLoad, onError                            │
├──────────────┼──────────────────────────────────────────┤
│ Composition  │ onCompositionStart, onCompositionUpdate,   │
│              │ onCompositionEnd                           │
├──────────────┼──────────────────────────────────────────┤
│ Pointer      │ onPointerDown, onPointerMove,              │
│              │ onPointerUp, onPointerCancel,              │
│              │ onPointerEnter, onPointerLeave             │
└──────────────┴──────────────────────────────────────────┘

11. Mouse Events Deep Dive

onClick — The Most Common Event

function ClickVariations() {
  function handleClick(event) {
    // Detect modifier keys for power-user features
    if (event.metaKey || event.ctrlKey) {
      console.log('Cmd/Ctrl + Click → open in new tab');
    } else if (event.shiftKey) {
      console.log('Shift + Click → select range');
    } else {
      console.log('Normal click');
    }
  }

  return <button onClick={handleClick}>Smart Click</button>;
}

onDoubleClick

function DoubleClickEdit() {
  const [isEditing, setIsEditing] = useState(false);
  const [text, setText] = useState('Double-click me to edit');

  if (isEditing) {
    return (
      <input
        value={text}
        onChange={e => setText(e.target.value)}
        onBlur={() => setIsEditing(false)}
        onKeyDown={e => e.key === 'Enter' && setIsEditing(false)}
        autoFocus
      />
    );
  }

  return <p onDoubleClick={() => setIsEditing(true)}>{text}</p>;
}

onMouseEnter vs onMouseOver

function HoverDemo() {
  const [isHovered, setIsHovered] = useState(false);

  return (
    <div
      // onMouseEnter/Leavedoesn't trigger for children
      onMouseEnter={() => setIsHovered(true)}
      onMouseLeave={() => setIsHovered(false)}
      style={{
        padding: 20,
        background: isHovered ? '#e0f0ff' : '#f0f0f0',
        transition: 'background 0.2s',
      }}
    >
      <p>Hover over me</p>
      <button>This button won't trigger extra events</button>
    </div>
  );
}
EventBubbles?When it fires
onMouseEnterNoMouse enters element (ignores children)
onMouseLeaveNoMouse leaves element (ignores children)
onMouseOverYesMouse enters element OR any child
onMouseOutYesMouse leaves element OR any child

Rule: Almost always use onMouseEnter/onMouseLeave — they're simpler and don't cause flicker.

Drag-and-Drop (Basic)

function DragDropDemo() {
  const [items, setItems] = useState(['Item A', 'Item B', 'Item C']);
  const [dragIndex, setDragIndex] = useState(null);

  function handleDragStart(index) {
    setDragIndex(index);
  }

  function handleDragOver(event) {
    event.preventDefault(); // Required to allow drop
  }

  function handleDrop(dropIndex) {
    const newItems = [...items];
    const [dragged] = newItems.splice(dragIndex, 1);
    newItems.splice(dropIndex, 0, dragged);
    setItems(newItems);
    setDragIndex(null);
  }

  return (
    <ul>
      {items.map((item, index) => (
        <li
          key={item}
          draggable
          onDragStart={() => handleDragStart(index)}
          onDragOver={handleDragOver}
          onDrop={() => handleDrop(index)}
          style={{
            padding: 10,
            margin: 4,
            background: dragIndex === index ? '#ccc' : '#fff',
            border: '1px solid #ddd',
            cursor: 'grab',
          }}
        >
          {item}
        </li>
      ))}
    </ul>
  );
}

12. Keyboard Events Deep Dive

onKeyDown vs onKeyUp

function KeyboardDemo() {
  function handleKeyDown(event) {
    console.log('Key down:', event.key, event.code);
    
    // Common keyboard shortcuts
    if (event.key === 'Enter') {
      console.log('Enter pressed');
    }
    if (event.key === 'Escape') {
      console.log('Escape pressed');
    }
    if ((event.metaKey || event.ctrlKey) && event.key === 's') {
      event.preventDefault(); // Prevent browser save dialog
      console.log('Cmd/Ctrl+S — save');
    }
    if ((event.metaKey || event.ctrlKey) && event.key === 'k') {
      event.preventDefault();
      console.log('Cmd/Ctrl+K — open command palette');
    }
  }

  return (
    <div tabIndex={0} onKeyDown={handleKeyDown}>
      <p>Focus here and press keys</p>
    </div>
  );
}

Key vs Code

// event.key  — the CHARACTER produced (affected by keyboard layout)
// event.code — the PHYSICAL key (same regardless of layout)

// On US keyboard, pressing "Z":
// key: "z"     code: "KeyZ"

// On German keyboard, pressing the same physical key:
// key: "y"     code: "KeyZ"   ← code is still KeyZ!
PropertyReturnsUse for
event.keyThe character ('a', 'Enter', 'Escape')Text input, shortcuts by character
event.codePhysical key ('KeyA', 'Enter', 'Escape')Games, keyboard-layout-independent shortcuts

Keyboard-Accessible Button

function AccessibleCustomButton({ onClick, children }) {
  function handleKeyDown(event) {
    // Space or Enter activates button
    if (event.key === ' ' || event.key === 'Enter') {
      event.preventDefault(); // Prevent scrolling on space
      onClick();
    }
  }

  return (
    <div
      role="button"
      tabIndex={0}
      onClick={onClick}
      onKeyDown={handleKeyDown}
      style={{ cursor: 'pointer', display: 'inline-block' }}
    >
      {children}
    </div>
  );
}

Global Keyboard Shortcuts Hook

function useKeyboardShortcut(key, callback, modifiers = {}) {
  useEffect(() => {
    function handleKeyDown(event) {
      const matchesKey = event.key.toLowerCase() === key.toLowerCase();
      const matchesMeta = !modifiers.meta || event.metaKey || event.ctrlKey;
      const matchesShift = !modifiers.shift || event.shiftKey;
      const matchesAlt = !modifiers.alt || event.altKey;

      if (matchesKey && matchesMeta && matchesShift && matchesAlt) {
        event.preventDefault();
        callback(event);
      }
    }

    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [key, callback, modifiers]);
}

// Usage
function App() {
  useKeyboardShortcut('k', () => openCommandPalette(), { meta: true });
  useKeyboardShortcut('Escape', () => closeModal());
  useKeyboardShortcut('s', () => save(), { meta: true });
  // ...
}

13. Focus and Blur Events

Basic Focus/Blur

function FocusDemo() {
  const [isFocused, setIsFocused] = useState(false);

  return (
    <input
      type="text"
      placeholder="Focus me"
      onFocus={() => setIsFocused(true)}
      onBlur={() => setIsFocused(false)}
      style={{
        border: `2px solid ${isFocused ? '#0070f3' : '#ccc'}`,
        outline: 'none',
        padding: '8px 12px',
        borderRadius: 4,
        transition: 'border-color 0.2s',
      }}
    />
  );
}

Focus Within (Container Focus Tracking)

function FocusWithinDemo() {
  const [hasFocusWithin, setHasFocusWithin] = useState(false);

  return (
    <div
      onFocus={() => setHasFocusWithin(true)}
      onBlur={(event) => {
        // Check if focus moved OUTSIDE this container
        if (!event.currentTarget.contains(event.relatedTarget)) {
          setHasFocusWithin(false);
        }
      }}
      style={{
        padding: 20,
        border: `2px solid ${hasFocusWithin ? '#0070f3' : '#eee'}`,
        borderRadius: 8,
      }}
    >
      <input type="text" placeholder="First name" />
      <input type="text" placeholder="Last name" />
      <input type="email" placeholder="Email" />
    </div>
  );
}

relatedTarget — Where Focus Came From / Went To

function RelatedTargetDemo() {
  function handleBlur(event) {
    console.log('Lost focus from:', event.target);
    console.log('Focus moved to:', event.relatedTarget);
    
    // If relatedTarget is null, focus left the window
    if (!event.relatedTarget) {
      console.log('User tabbed out of the browser');
    }
  }

  return <input onBlur={handleBlur} />;
}

14. Touch Events for Mobile

Basic Touch Handling

function TouchDemo() {
  const [touchPos, setTouchPos] = useState(null);

  function handleTouchStart(event) {
    const touch = event.touches[0];
    setTouchPos({ x: touch.clientX, y: touch.clientY });
  }

  function handleTouchMove(event) {
    const touch = event.touches[0];
    setTouchPos({ x: touch.clientX, y: touch.clientY });
  }

  function handleTouchEnd() {
    setTouchPos(null);
  }

  return (
    <div
      onTouchStart={handleTouchStart}
      onTouchMove={handleTouchMove}
      onTouchEnd={handleTouchEnd}
      style={{ width: 300, height: 300, background: '#f0f0f0', position: 'relative' }}
    >
      {touchPos && (
        <div
          style={{
            position: 'absolute',
            left: touchPos.x - 10,
            top: touchPos.y - 10,
            width: 20,
            height: 20,
            borderRadius: '50%',
            background: 'blue',
          }}
        />
      )}
    </div>
  );
}

Swipe Detection

function useSwipe(onSwipeLeft, onSwipeRight, threshold = 50) {
  const touchStartX = useRef(null);

  function handleTouchStart(event) {
    touchStartX.current = event.touches[0].clientX;
  }

  function handleTouchEnd(event) {
    if (touchStartX.current === null) return;

    const touchEndX = event.changedTouches[0].clientX;
    const diff = touchStartX.current - touchEndX;

    if (Math.abs(diff) > threshold) {
      if (diff > 0) {
        onSwipeLeft?.();
      } else {
        onSwipeRight?.();
      }
    }

    touchStartX.current = null;
  }

  return { onTouchStart: handleTouchStart, onTouchEnd: handleTouchEnd };
}

// Usage
function Carousel() {
  const [slide, setSlide] = useState(0);
  const swipeHandlers = useSwipe(
    () => setSlide(s => s + 1),  // left swipe → next
    () => setSlide(s => s - 1),  // right swipe → prev
  );

  return <div {...swipeHandlers}>Slide {slide}</div>;
}

15. Event Delegation in React — You Don't Need It

In vanilla JS, event delegation is a performance optimization where you attach one listener to a parent instead of many listeners to children. React already does this for you.

Vanilla JS Delegation (Manual)

// Without delegation — BAD for 1000 items
document.querySelectorAll('.item').forEach(item => {
  item.addEventListener('click', handleClick);
});

// With delegation — one listener
document.querySelector('.list').addEventListener('click', (event) => {
  if (event.target.matches('.item')) {
    handleClick(event);
  }
});

React — Delegation is Built In

// React handles delegation internally
// You write this, React optimizes it
function ItemList({ items }) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id} onClick={() => handleClick(item.id)}>
          {item.name}
        </li>
      ))}
    </ul>
  );
}
// React attaches ONE listener at the root, not 1000 on each <li>

You never need manual event delegation in React. Just write handlers normally and React handles it.


16. TypeScript and Events

Typing Event Handlers

// Method 1: Type the event parameter
function ClickHandler() {
  function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
    console.log(event.clientX);
  }
  return <button onClick={handleClick}>Click</button>;
}

// Method 2: Type the handler function
function FormHandler() {
  const handleSubmit: React.FormEventHandler<HTMLFormElement> = (event) => {
    event.preventDefault();
    const formData = new FormData(event.currentTarget);
  };
  return <form onSubmit={handleSubmit}>...</form>;
}

Common Event Types in TypeScript

// Mouse events
React.MouseEvent<HTMLButtonElement>      // onClick, onDoubleClick
React.MouseEvent<HTMLDivElement>         // onMouseEnter, onMouseLeave

// Keyboard events
React.KeyboardEvent<HTMLInputElement>    // onKeyDown, onKeyUp

// Form events
React.FormEvent<HTMLFormElement>         // onSubmit
React.ChangeEvent<HTMLInputElement>      // onChange
React.ChangeEvent<HTMLSelectElement>     // onChange for select
React.ChangeEvent<HTMLTextAreaElement>   // onChange for textarea

// Focus events
React.FocusEvent<HTMLInputElement>       // onFocus, onBlur

// Drag events
React.DragEvent<HTMLDivElement>          // onDrag, onDrop

// Touch events
React.TouchEvent<HTMLDivElement>         // onTouchStart, onTouchEnd

// Clipboard events
React.ClipboardEvent<HTMLInputElement>   // onCopy, onPaste

// Scroll events
React.UIEvent<HTMLDivElement>            // onScroll

Generic Reusable Handler

function useEventHandler<E extends HTMLElement>(
  handler: (event: React.MouseEvent<E>) => void
) {
  return useCallback(handler, [handler]);
}

17. Performance Considerations

Problem: New Functions on Every Render

function List({ items, onItemClick }) {
  return (
    <ul>
      {items.map(item => (
        // ⚠️ Creates a new function every render
        <li key={item.id} onClick={() => onItemClick(item.id)}>
          {item.name}
        </li>
      ))}
    </ul>
  );
}

Solution 1: useCallback for Stable References

function List({ items, onItemClick }) {
  const handleClick = useCallback((id) => {
    onItemClick(id);
  }, [onItemClick]);

  return (
    <ul>
      {items.map(item => (
        <MemoizedItem key={item.id} item={item} onClick={handleClick} />
      ))}
    </ul>
  );
}

const MemoizedItem = React.memo(function Item({ item, onClick }) {
  return <li onClick={() => onClick(item.id)}>{item.name}</li>;
});

Solution 2: Data Attributes (Zero Extra Functions)

function List({ items }) {
  function handleClick(event) {
    const id = event.currentTarget.dataset.id;
    console.log('Clicked item:', id);
  }

  return (
    <ul>
      {items.map(item => (
        <li key={item.id} data-id={item.id} onClick={handleClick}>
          {item.name}
        </li>
      ))}
    </ul>
  );
}

When Performance Actually Matters

ScenarioOptimize?Why
List < 100 itemsNoNegligible overhead
List > 500 itemsMaybeTest first with profiler
Handler triggers heavy computationYesuseCallback + useMemo
Passing handler to React.memo childYesPrevents unnecessary re-renders
Simple button clickNoOver-optimization is worse

React Profiler: Open DevTools → Profiler tab → Record → Click around → See which renders were unnecessary.


18. Common Mistakes and Anti-Patterns

Mistake 1: Calling Instead of Passing

// ❌ WRONG — calls handleClick during render
<button onClick={handleClick()}>Click</button>

// ✅ CORRECT — passes function reference
<button onClick={handleClick}>Click</button>

// ✅ CORRECT — arrow wrapper for arguments
<button onClick={() => handleClick('arg')}>Click</button>

Mistake 2: Forgetting preventDefault on Forms

// ❌ WRONG — page reloads
function Form() {
  function handleSubmit() {
    console.log('Submitted'); // You'll never see this — page reloads
  }
  return <form onSubmit={handleSubmit}><button>Submit</button></form>;
}

// ✅ CORRECT
function Form() {
  function handleSubmit(event) {
    event.preventDefault();
    console.log('Submitted');
  }
  return <form onSubmit={handleSubmit}><button>Submit</button></form>;
}

Mistake 3: Not Stopping Propagation in Nested Interactives

// ❌ WRONG — clicking delete opens card detail
function Card({ onOpen, onDelete }) {
  return (
    <div onClick={onOpen}>
      <h3>Card Title</h3>
      <button onClick={onDelete}>Delete</button>  {/* Also triggers onOpen! */}
    </div>
  );
}

// ✅ CORRECT
function Card({ onOpen, onDelete }) {
  function handleDelete(event) {
    event.stopPropagation();
    onDelete();
  }
  return (
    <div onClick={onOpen}>
      <h3>Card Title</h3>
      <button onClick={handleDelete}>Delete</button>
    </div>
  );
}

Mistake 4: Async Event Properties

// ❌ WRONG — event properties may be null in async callback (React 16)
function AsyncBug() {
  function handleClick(event) {
    setTimeout(() => {
      console.log(event.target); // May be null in React 16!
    }, 100);
  }
  return <button onClick={handleClick}>Click</button>;
}

// ✅ CORRECT — extract values first
function AsyncFix() {
  function handleClick(event) {
    const target = event.target; // Extract synchronously
    setTimeout(() => {
      console.log(target); // Always works
    }, 100);
  }
  return <button onClick={handleClick}>Click</button>;
}

// Note: React 17+ no longer pools events, so this is less of an issue.
// But extracting values is still good practice.

Mistake 5: Missing Event Argument

// ❌ WRONG — forgot event parameter
function WrongHandler() {
  function handleSubmit() {
    event.preventDefault(); // 'event' is undefined!
  }
  return <form onSubmit={handleSubmit}>...</form>;
}

// ✅ CORRECT — event is the first parameter
function CorrectHandler() {
  function handleSubmit(event) {
    event.preventDefault();
  }
  return <form onSubmit={handleSubmit}>...</form>;
}

19. Real-World Examples

Like Button with Animation

function LikeButton({ initialLiked = false, initialCount = 0 }) {
  const [liked, setLiked] = useState(initialLiked);
  const [count, setCount] = useState(initialCount);
  const [animating, setAnimating] = useState(false);

  function handleClick() {
    setLiked(!liked);
    setCount(prev => liked ? prev - 1 : prev + 1);
    setAnimating(true);
    setTimeout(() => setAnimating(false), 300);
  }

  return (
    <button
      onClick={handleClick}
      style={{
        background: 'none',
        border: '1px solid #eee',
        padding: '8px 16px',
        borderRadius: 20,
        cursor: 'pointer',
        transform: animating ? 'scale(1.2)' : 'scale(1)',
        transition: 'transform 0.3s ease',
      }}
    >
      <span style={{ color: liked ? 'red' : '#999' }}>
        {liked ? '❤️' : '🤍'}
      </span>
      {' '}{count}
    </button>
  );
}

Context Menu

function ContextMenu() {
  const [menu, setMenu] = useState(null);

  function handleContextMenu(event) {
    event.preventDefault();
    setMenu({ x: event.clientX, y: event.clientY });
  }

  function handleClick() {
    setMenu(null); // Close menu on any click
  }

  useEffect(() => {
    document.addEventListener('click', handleClick);
    return () => document.removeEventListener('click', handleClick);
  }, []);

  return (
    <div onContextMenu={handleContextMenu} style={{ height: 300, background: '#f5f5f5' }}>
      <p>Right-click anywhere in this area</p>

      {menu && (
        <div
          style={{
            position: 'fixed',
            left: menu.x,
            top: menu.y,
            background: 'white',
            boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
            borderRadius: 8,
            padding: 8,
            zIndex: 1000,
          }}
        >
          <button onClick={() => console.log('Edit')}>✏️ Edit</button><br />
          <button onClick={() => console.log('Copy')}>📋 Copy</button><br />
          <button onClick={() => console.log('Delete')}>🗑️ Delete</button>
        </div>
      )}
    </div>
  );
}

Debounced Search Input

function DebouncedSearch() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const timeoutRef = useRef(null);

  function handleChange(event) {
    const value = event.target.value;
    setQuery(value);

    // Debounce: wait 300ms after user stops typing
    clearTimeout(timeoutRef.current);
    timeoutRef.current = setTimeout(() => {
      if (value.trim()) {
        fetchResults(value).then(setResults);
      } else {
        setResults([]);
      }
    }, 300);
  }

  // Cleanup on unmount
  useEffect(() => {
    return () => clearTimeout(timeoutRef.current);
  }, []);

  return (
    <div>
      <input
        type="search"
        value={query}
        onChange={handleChange}
        placeholder="Search..."
      />
      <ul>
        {results.map(r => <li key={r.id}>{r.name}</li>)}
      </ul>
    </div>
  );
}

20. Key Takeaways

  1. React uses SyntheticEvents — cross-browser wrappers with the same API as native events
  2. Event names are camelCase in JSX — onClick not onclick
  3. Pass function references, don't call them — onClick={fn} not onClick={fn()}
  4. event.target is what was clicked; event.currentTarget is where the handler lives
  5. preventDefault() stops browser defaults; stopPropagation() stops bubbling
  6. React handles event delegation — you never need to manually delegate
  7. Name handlers handleX, name props onX
  8. Functional components have no this problem — one of the main reasons they won
  9. Performance rarely matters for event handlers — profile before optimizing
  10. TypeScript event types follow the pattern React.{EventType}<{HTMLElement}>

Explain-It Challenge

  1. The Cocktail Party Explanation: Explain to a non-developer friend how React handles user clicks differently from a plain HTML button. Use the analogy of a receptionist at a company — all calls go through one person who routes them to the right department.

  2. The Junior Developer Walkthrough: A junior developer's form keeps reloading the page when submitted. Walk them through debugging it — what's the cause, the fix, and the underlying concept they need to understand.

  3. The Architecture Discussion: In a component hierarchy where a Card wraps a Button and both need click handlers, explain why stopPropagation() is necessary and when you might NOT want to use it (hint: analytics, global close handlers).


Navigation: ← Overview · Next → Controlled Inputs