Skip to main content

Handling Events

Events are how users interact with your application. React provides a consistent, cross-browser event system called Synthetic Events.

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!

// ❌ WRONG - Function is called immediately on render
<button onClick={handleClick()}>Click</button>

// ✅ CORRECT - Function reference passed, called on click
<button onClick={handleClick}>Click</button>

// ✅ CORRECT - Arrow function called on click
<button onClick={() => handleClick()}>Click</button>

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>
  );
}

🎯 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!