Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

Handling Events

Events are how users interact with your application. Every click, keystroke, form submission, and mouse movement fires an event that your components can respond to. React provides a consistent, cross-browser event system called Synthetic Events. Think of SyntheticEvents as a universal translator: no matter which browser your user is on (Chrome, Firefox, Safari), React normalizes the event into a single, consistent API so you never have to write browser-specific code.

React Events vs DOM Events

FeatureHTML/DOMReact
Naminglowercase (onclick)camelCase (onClick)
HandlerString ("handleClick()")Function ({handleClick})
Prevent defaultreturn false;e.preventDefault()
Event objectBrowser-specificNormalized (Synthetic)
HTML:
<button onclick="handleClick()">Click</button>
React:
<button onClick={handleClick}>Click</button>

Basic Event Handling

Defining Event Handlers

function Button() {
  // Method 1: Separate function (recommended for complex logic)
  const handleClick = () => {
    console.log('Button clicked!');
  };

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

// Method 2: Inline function (ok for simple cases)
function Button() {
  return (
    <button onClick={() => console.log('Clicked!')}>
      Click Me
    </button>
  );
}
Best Practice: Define handlers as separate functions for:
  • Better readability
  • Easier debugging
  • Potential performance benefits with useCallback

Don’t Call the Function!

This is one of the most common beginner mistakes in React. You need to pass a reference to a function, not the result of calling it.
// ❌ WRONG - Parentheses () mean "call this right now!"
// handleClick runs during render, not on click.
// If handleClick calls setState, you get an infinite render loop.
<button onClick={handleClick()}>Click</button>

// ✅ CORRECT - Pass the function reference (no parentheses)
// React will call it later when the user actually clicks.
<button onClick={handleClick}>Click</button>

// ✅ CORRECT - Wrap in an arrow function if you need to pass arguments
<button onClick={() => handleClick()}>Click</button>
Practical pitfall: If you see your handler firing on every render (or your app freezing in an infinite loop), check for accidental function invocation with (). This is the #1 debugging timesink for React beginners.

The Event Object

React wraps native browser events in a SyntheticEvent for cross-browser compatibility.
function Form() {
  const handleSubmit = (event) => {
    event.preventDefault(); // Stop form from refreshing page
    console.log('Form submitted');
  };

  const handleChange = (event) => {
    console.log('Target element:', event.target);
    console.log('Input value:', event.target.value);
    console.log('Input name:', event.target.name);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input 
        name="email" 
        onChange={handleChange}
        placeholder="Email"
      />
      <button type="submit">Submit</button>
    </form>
  );
}

Common Event Properties

PropertyDescription
event.targetElement that triggered the event
event.currentTargetElement the handler is attached to
event.typeEvent type ('click', 'change', etc.)
event.preventDefault()Prevents default browser behavior
event.stopPropagation()Stops event bubbling
event.nativeEventUnderlying native browser event

Passing Arguments to Event Handlers

Often you need to pass data (like an item ID) to the handler.
function ProductList({ products }) {
  const handleDelete = (productId) => {
    console.log('Deleting product:', productId);
  };

  const handleEdit = (productId, currentName) => {
    console.log('Editing:', productId, currentName);
  };

  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>
          {product.name}
          {/* Arrow function wrapper */}
          <button onClick={() => handleDelete(product.id)}>
            Delete
          </button>
          <button onClick={() => handleEdit(product.id, product.name)}>
            Edit
          </button>
        </li>
      ))}
    </ul>
  );
}

Accessing Event AND Custom Arguments

const handleClick = (productId, event) => {
  event.preventDefault();
  console.log('Product:', productId);
  console.log('Button:', event.target);
};

<button onClick={(e) => handleClick(product.id, e)}>
  Click
</button>

Mouse Events

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

  return (
    <div
      style={{
        backgroundColor: isHovered ? 'lightblue' : 'lightgray',
        padding: '20px',
        cursor: 'pointer'
      }}
      onClick={() => console.log('Clicked!')}
      onDoubleClick={() => console.log('Double clicked!')}
      onMouseEnter={() => setIsHovered(true)}
      onMouseLeave={() => setIsHovered(false)}
      onMouseMove={(e) => console.log(`Position: ${e.clientX}, ${e.clientY}`)}
      onContextMenu={(e) => {
        e.preventDefault(); // Prevent right-click menu
        console.log('Right clicked!');
      }}
    >
      Hover and Click Me
    </div>
  );
}

Mouse Event Types

EventTriggers When
onClickMouse button clicked
onDoubleClickMouse button double-clicked
onMouseEnterCursor enters element (doesn’t bubble)
onMouseLeaveCursor leaves element (doesn’t bubble)
onMouseOverCursor over element (bubbles)
onMouseOutCursor leaves element (bubbles)
onMouseMoveCursor moves over element
onMouseDownMouse button pressed
onMouseUpMouse button released
onContextMenuRight-click (context menu)

Keyboard Events

function SearchInput() {
  const [query, setQuery] = useState('');

  const handleKeyDown = (event) => {
    // Check for specific keys
    if (event.key === 'Enter') {
      console.log('Searching for:', query);
      // Perform search
    }
    
    if (event.key === 'Escape') {
      setQuery('');
    }

    // Keyboard shortcuts with modifiers
    if (event.ctrlKey && event.key === 'k') {
      event.preventDefault();
      console.log('Ctrl+K pressed - Open command palette');
    }
  };

  return (
    <input
      type="text"
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      onKeyDown={handleKeyDown}
      placeholder="Search... (Enter to submit, Esc to clear)"
    />
  );
}

Key Event Properties

PropertyDescription
event.keyThe key value ('Enter', 'a', 'ArrowUp')
event.codePhysical key code ('KeyA', 'Enter')
event.altKeyAlt key held
event.ctrlKeyCtrl key held
event.shiftKeyShift key held
event.metaKeyMeta key held (Cmd on Mac)

Building Keyboard Shortcuts

function App() {
  useEffect(() => {
    const handleGlobalKeyDown = (event) => {
      // Ctrl/Cmd + S to save
      if ((event.ctrlKey || event.metaKey) && event.key === 's') {
        event.preventDefault();
        saveDocument();
      }
      
      // Ctrl/Cmd + Shift + P for command palette
      if ((event.ctrlKey || event.metaKey) && event.shiftKey && event.key === 'P') {
        event.preventDefault();
        openCommandPalette();
      }
    };

    window.addEventListener('keydown', handleGlobalKeyDown);
    return () => window.removeEventListener('keydown', handleGlobalKeyDown);
  }, []);

  return <div>...</div>;
}

Form Events

Input Change Event

function FormExample() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    agree: false
  });

  const handleChange = (event) => {
    const { name, value, type, checked } = event.target;
    setFormData(prev => ({
      ...prev,
      [name]: type === 'checkbox' ? checked : value
    }));
  };

  return (
    <form>
      <input
        name="name"
        value={formData.name}
        onChange={handleChange}
      />
      <input
        name="email"
        type="email"
        value={formData.email}
        onChange={handleChange}
      />
      <input
        name="agree"
        type="checkbox"
        checked={formData.agree}
        onChange={handleChange}
      />
    </form>
  );
}

Focus Events

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

  return (
    <div>
      <input
        onFocus={() => setIsFocused(true)}
        onBlur={() => setIsFocused(false)}
        style={{
          borderColor: isFocused ? 'blue' : 'gray',
          outline: 'none'
        }}
      />
      {isFocused && <p>Input is focused!</p>}
    </div>
  );
}

Event Propagation

Events bubble up through the DOM tree. You can stop this behavior.
function Card() {
  const handleCardClick = () => {
    console.log('Card clicked');
  };

  const handleButtonClick = (event) => {
    event.stopPropagation(); // Prevents handleCardClick from firing
    console.log('Button clicked');
  };

  return (
    <div onClick={handleCardClick} className="card">
      <h2>Card Title</h2>
      <button onClick={handleButtonClick}>
        Click Me (doesn't trigger card click)
      </button>
    </div>
  );
}

Event Capturing

React also supports the capture phase (rarely needed):
<div onClickCapture={() => console.log('Capture phase')}>
  <button onClick={() => console.log('Bubble phase')}>
    Click
  </button>
</div>
// Logs: "Capture phase" then "Bubble phase"

Touch Events (Mobile)

function SwipeableCard() {
  const [touchStart, setTouchStart] = useState(null);
  const [touchEnd, setTouchEnd] = useState(null);

  const minSwipeDistance = 50;

  const onTouchStart = (e) => {
    setTouchEnd(null);
    setTouchStart(e.targetTouches[0].clientX);
  };

  const onTouchMove = (e) => {
    setTouchEnd(e.targetTouches[0].clientX);
  };

  const onTouchEnd = () => {
    if (!touchStart || !touchEnd) return;
    
    const distance = touchStart - touchEnd;
    const isLeftSwipe = distance > minSwipeDistance;
    const isRightSwipe = distance < -minSwipeDistance;
    
    if (isLeftSwipe) console.log('Swiped left');
    if (isRightSwipe) console.log('Swiped right');
  };

  return (
    <div
      onTouchStart={onTouchStart}
      onTouchMove={onTouchMove}
      onTouchEnd={onTouchEnd}
      className="swipeable-card"
    >
      Swipe me!
    </div>
  );
}

Drag and Drop Events

function DragDropExample() {
  const [draggedItem, setDraggedItem] = useState(null);
  const [items, setItems] = useState(['Item 1', 'Item 2', 'Item 3']);

  const handleDragStart = (e, index) => {
    setDraggedItem(index);
    e.dataTransfer.effectAllowed = 'move';
  };

  const handleDragOver = (e) => {
    e.preventDefault();
    e.dataTransfer.dropEffect = 'move';
  };

  const handleDrop = (e, dropIndex) => {
    e.preventDefault();
    const newItems = [...items];
    const [removed] = newItems.splice(draggedItem, 1);
    newItems.splice(dropIndex, 0, removed);
    setItems(newItems);
    setDraggedItem(null);
  };

  return (
    <ul>
      {items.map((item, index) => (
        <li
          key={item}
          draggable
          onDragStart={(e) => handleDragStart(e, index)}
          onDragOver={handleDragOver}
          onDrop={(e) => handleDrop(e, index)}
          style={{
            opacity: draggedItem === index ? 0.5 : 1,
            cursor: 'move'
          }}
        >
          {item}
        </li>
      ))}
    </ul>
  );
}

Event Handler Pitfalls

Pitfall 1 — Inline arrow functions in lists cause unnecessary re-renders: When you write onClick={() => handleDelete(item.id)} inside a .map(), a new function is created for every item on every render. If the child component is wrapped in React.memo, this defeats memoization because the onClick prop is always a new reference. For large lists, extract the handler or use useCallback.Pitfall 2 — SyntheticEvent pooling (React 16 and earlier): In older React versions, event objects were pooled and reused. Accessing event.target inside a setTimeout or async function returned null because the event had been recycled. React 17+ eliminated pooling, but you may encounter this in legacy codebases. The fix was event.persist().Pitfall 3 — stopPropagation hiding bugs: Using event.stopPropagation() prevents an event from reaching parent handlers. This can silently break features like a “click outside to close” listener on the document, because the click event never reaches the document. Use it sparingly and document why.Pitfall 4 — Double-click interfering with single click: If you listen to both onClick and onDoubleClick on the same element, a double-click fires two onClick events before the onDoubleClick. To distinguish between them, use a timer: wait ~250ms after a click before acting, and cancel it if a double-click arrives.

🎯 Practice Exercises

function LikeButton() {
  const [likes, setLikes] = useState(0);
  const [superLiked, setSuperLiked] = useState(false);

  const handleClick = () => {
    if (!superLiked) {
      setLikes(prev => prev + 1);
    }
  };

  const handleDoubleClick = () => {
    setSuperLiked(true);
    setLikes(prev => prev + 10);
  };

  return (
    <button
      onClick={handleClick}
      onDoubleClick={handleDoubleClick}
      style={{
        backgroundColor: superLiked ? 'gold' : 'pink',
        border: 'none',
        padding: '10px 20px',
        borderRadius: '20px',
        cursor: 'pointer'
      }}
    >
      {superLiked ? '⭐' : '❤️'} {likes}
    </button>
  );
}
function Character() {
  const [position, setPosition] = useState({ x: 50, y: 50 });
  const step = 10;

  useEffect(() => {
    const handleKeyDown = (e) => {
      switch (e.key) {
        case 'ArrowUp':
        case 'w':
          setPosition(p => ({ ...p, y: Math.max(0, p.y - step) }));
          break;
        case 'ArrowDown':
        case 's':
          setPosition(p => ({ ...p, y: Math.min(90, p.y + step) }));
          break;
        case 'ArrowLeft':
        case 'a':
          setPosition(p => ({ ...p, x: Math.max(0, p.x - step) }));
          break;
        case 'ArrowRight':
        case 'd':
          setPosition(p => ({ ...p, x: Math.min(90, p.x + step) }));
          break;
      }
    };

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

  return (
    <div style={{
      position: 'relative',
      width: '300px',
      height: '300px',
      border: '1px solid black'
    }}>
      <div style={{
        position: 'absolute',
        left: `${position.x}%`,
        top: `${position.y}%`,
        width: '20px',
        height: '20px',
        backgroundColor: 'blue',
        borderRadius: '50%',
        transition: 'all 0.1s'
      }} />
      <p>Use WASD or Arrow keys to move</p>
    </div>
  );
}
function Dropdown() {
  const [isOpen, setIsOpen] = useState(false);
  const dropdownRef = useRef(null);

  useEffect(() => {
    const handleClickOutside = (event) => {
      if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
        setIsOpen(false);
      }
    };

    document.addEventListener('mousedown', handleClickOutside);
    return () => document.removeEventListener('mousedown', handleClickOutside);
  }, []);

  return (
    <div ref={dropdownRef} className="dropdown">
      <button onClick={() => setIsOpen(!isOpen)}>
        {isOpen ? 'Close' : 'Open'} Menu
      </button>
      {isOpen && (
        <ul className="dropdown-menu">
          <li>Option 1</li>
          <li>Option 2</li>
          <li>Option 3</li>
        </ul>
      )}
    </div>
  );
}

Summary

ConceptDescription
Event HandlersFunctions that respond to user interactions
SyntheticEventReact’s cross-browser event wrapper
event.targetElement that triggered the event
event.preventDefault()Stop default browser behavior
event.stopPropagation()Stop event bubbling
Mouse Eventsclick, doubleClick, mouseEnter, mouseLeave, etc.
Keyboard EventskeyDown, keyUp, keyPress (use key property)
Form EventsonChange, onSubmit, onFocus, onBlur
Touch EventstouchStart, touchMove, touchEnd (mobile)

Next Steps

In the next chapter, you’ll learn about Lists & Keys — efficiently rendering collections of data!

Interview Deep-Dive

Strong Answer: SyntheticEvents are React’s cross-browser wrapper around native DOM events. They exist because browser event implementations are inconsistent — different browsers name properties differently, bubble events differently, and handle default behaviors differently. React normalizes all of this into a single consistent API so you write event handling code once and it works everywhere.Under the hood, React does not attach event listeners to individual DOM nodes. Since React 17, it attaches a single event listener to the root DOM container (the element you pass to createRoot). When an event fires, it bubbles up to the root, React identifies which component the event target belongs to, creates a SyntheticEvent wrapper, and dispatches it through the React component tree. This is called event delegation, and it is more memory-efficient than attaching thousands of individual listeners.The key behavioral differences from native events: SyntheticEvents are pooled in React 16 and earlier (the event object is reused across handlers, so you cannot access it asynchronously without calling e.persist()). React 17+ removed pooling, so events are regular objects you can access anytime. Also, stopPropagation() on a SyntheticEvent stops propagation in React’s event system but does not stop the native event from propagating — this can cause subtle bugs if you mix React handlers with native addEventListener calls.Follow-up: You add a native event listener via addEventListener in a useEffect, and a React onClick handler on the same element. The stopPropagation in the React handler does not prevent the native listener from firing. Why?Because React 17+ attaches its delegated listener to the root container, not the individual element. When you click the element, the native event propagates through the real DOM first. Your addEventListener handler on the element fires during native propagation. Then the event reaches the root container, React intercepts it, creates a SyntheticEvent, and dispatches it through the React tree. e.stopPropagation() in the React handler stops propagation within React’s virtual event system, but the native propagation already happened.To prevent the native listener from firing, you need to call e.nativeEvent.stopImmediatePropagation() in the React handler, which stops the underlying native event. But this is fragile and usually signals an architectural problem — mixing React and native event handling on the same element is a code smell. Better to handle everything in React or everything natively, not both.
Strong Answer: Event delegation is the pattern of attaching a single event listener to a parent element and using event.target to determine which child triggered it. React uses this internally — instead of attaching an onClick to every button in your app, React attaches one click listener to a container and routes events to the correct component handlers.Before React 17, this single listener was attached to document. In React 17, it moved to the root DOM container (document.getElementById('root')). This change seems minor but solved real production problems.The main issue was apps that gradually migrated to React. If you had a jQuery section and a React section on the same page, React’s listener on document would intercept events from the jQuery section. stopPropagation() called in React would prevent non-React event listeners on document from seeing the event. Moving to the root container isolates React’s event system to its own DOM subtree.It also fixed micro-frontend architectures where multiple React roots coexist. With document-level delegation, two React apps would interfere with each other’s event handling. Container-level delegation means each React root manages its own events independently.In practice, the only code that broke during this migration was code that relied on React events propagating to document — for example, a document.addEventListener('click', closeDropdown) that expected to fire after React’s click handlers. After React 17, the native document listener fires before React’s container-level handler, changing the order.Follow-up: In a performance-critical scenario with 10,000 list items, each needing a click handler, what are the implications of React’s event delegation?Event delegation is actually the ideal solution for this case. React does not attach 10,000 click listeners — it attaches one to the root and routes clicks based on the target. Memory usage is constant regardless of list size, which is exactly what you want for large lists.The caveat is that the routing still involves React identifying which component the click target belongs to, which involves walking up the fiber tree. For 10,000 items this is still O(depth of tree), not O(n), so it is fast. Combined with virtualization (only rendering visible items), the event handling cost is negligible.Where you might see performance issues is if each list item’s click handler is a new arrow function created on every render (onClick={() => handleClick(item.id)}). This does not affect event delegation, but it defeats React.memo on list items because the function reference changes every render. The fix is useCallback or passing the ID to a stable handler that reads it from the event.
Strong Answer: event.target is the element that actually triggered the event — the deepest element in the DOM that was clicked, typed in, or interacted with. event.currentTarget is the element that the event handler is attached to. They are the same when you click directly on the element with the handler, but they differ when the event bubbles.Concrete bug scenario: you have a card component with an onClick handler on the outer div. Inside the card there is a title h3 and a description p. The handler reads event.target.dataset.cardId to determine which card was clicked. If the user clicks on the h3, event.target is the h3, which does not have data-card-id — so dataset.cardId is undefined. The handler fails silently or navigates to the wrong route. The fix is to use event.currentTarget.dataset.cardId, which always refers to the div that has the handler (and the data attribute).This comes up constantly in list interfaces. You have a row with onClick on the <tr>, and inside there are <td> elements. event.target could be any <td> or even a <span> inside a <td>. If your handler assumes event.target is the <tr>, you will read wrong data.Another scenario: styling. You have event.target.classList.add('active') in an onClick handler on a container. If the user clicks a child element, you add the class to the child instead of the container. Use event.currentTarget for the element with the handler.Follow-up: How do capture and bubble phases work in React, and when would you use onClickCapture?In native DOM events, there are three phases: capture (event travels from document down to the target), target (event reaches the element), and bubble (event travels back up). React supports both capture and bubble phases through event props: onClick fires during the bubble phase, onClickCapture fires during the capture phase.The capture phase fires before the bubble phase, so onClickCapture on a parent fires before onClick on the child. This is useful when you need a parent to intercept an event before any child sees it.Practical use case: implementing a focus trap in a modal. You add onFocusCapture on the modal container to intercept any focus event before it reaches the target. If the focus target is outside the modal, you redirect focus back inside. Another use case: analytics logging at the app root that captures all clicks before any component’s handler might call stopPropagation. By using the capture phase, your logger always fires regardless of what child handlers do.