React Event Handling System

Understanding how to handle user interactions in React applications

Introduction to Events in React

Events are actions or occurrences that happen in the system you are building, which the system tells you about so you can respond to them in some way. In the context of web applications, events are typically user interactions like clicks, keyboard input, mouse movements, form submissions, and more.

React has its own event handling system which is similar to handling events in the DOM, but with some key differences and advantages.

flowchart TD A[User Interaction] --> B[DOM Event] B --> C[React SyntheticEvent] C --> D[Event Handler] D --> E[Component State Update] E --> F[Re-render] style C fill:#f9f,stroke:#333,stroke-width:2px

Key Differences from DOM Events

SyntheticEvent System

React implements a synthetic event system that wraps the browser's native event system. This provides several benefits:

Real-world analogy: Think of React's synthetic events like a universal translator. Different browsers might "speak different languages" when it comes to events, but React's synthetic event system translates everything into a common language your code can easily understand.

The Event Object

React's synthetic event objects follow the W3C spec, so they have the same properties and methods as native events:


function Button() {
  const handleClick = (event) => {
    // event is a SyntheticEvent
    console.log('Button clicked!');
    console.log('Event type:', event.type);
    console.log('Target:', event.target);
    
    // Accessing native event
    console.log('Native event:', event.nativeEvent);
  };
  
  return (
    <button onClick={handleClick}>
      Click Me
    </button>
  );
}
      

Note: React's SyntheticEvent is a cross-browser wrapper around the browser's native event. If you need access to the underlying browser event, you can use the nativeEvent property.

Common Event Types

Mouse Events

Event Description
onClick Fired when an element is clicked
onDoubleClick Fired when an element is double-clicked
onMouseEnter Fired when the mouse enters an element
onMouseLeave Fired when the mouse leaves an element
onMouseMove Fired when the mouse moves within an element

function MouseEventDemo() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const [isHovering, setIsHovering] = useState(false);
  
  const handleMouseMove = (e) => {
    // Update position relative to the container
    setPosition({
      x: e.nativeEvent.offsetX,
      y: e.nativeEvent.offsetY
    });
  };
  
  return (
    <div 
      style={{
        width: '300px',
        height: '200px',
        border: '1px solid #ccc',
        position: 'relative',
        backgroundColor: isHovering ? '#f0f0f0' : 'white'
      }}
      onMouseMove={handleMouseMove}
      onMouseEnter={() => setIsHovering(true)}
      onMouseLeave={() => setIsHovering(false)}
    >
      <p>Mouse position: {position.x}, {position.y}</p>
      {isHovering && (
        <div
          style={{
            position: 'absolute',
            left: position.x,
            top: position.y,
            width: '5px',
            height: '5px',
            borderRadius: '50%',
            backgroundColor: 'red',
            transform: 'translate(-50%, -50%)'
          }}
        />
      )}
    </div>
  );
}
      

Keyboard Events

Event Description
onKeyDown Fired when a key is pressed down
onKeyPress Fired when a key is pressed (character keys only)
onKeyUp Fired when a key is released

function KeyboardDemo() {
  const [lastKey, setLastKey] = useState('');
  const [inputValue, setInputValue] = useState('');
  
  const handleKeyDown = (e) => {
    setLastKey(e.key);
    
    // Example: Prevent typing numbers
    if (/^\d$/.test(e.key)) {
      e.preventDefault();
      alert('Numbers are not allowed!');
    }
  };
  
  return (
    <div>
      <p>Type something (no numbers allowed):</p>
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        onKeyDown={handleKeyDown}
        placeholder="Type here..."
      />
      {lastKey && <p>Last key pressed: {lastKey}</p>}
    </div>
  );
}
      

Form Events

Event Description
onChange Fired when the value of an input element changes
onSubmit Fired when a form is submitted
onFocus Fired when an element receives focus
onBlur Fired when an element loses focus

We'll cover form events in more detail in the next lecture, but here's a simple example:


function SimpleForm() {
  const [formData, setFormData] = useState({
    username: '',
    email: ''
  });
  
  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prevData => ({
      ...prevData,
      [name]: value
    }));
  };
  
  const handleSubmit = (e) => {
    e.preventDefault(); // Prevent page refresh
    console.log('Form submitted:', formData);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="username">Username:</label>
        <input
          id="username"
          name="username"
          value={formData.username}
          onChange={handleChange}
          onFocus={() => console.log('Username field focused')}
          onBlur={() => console.log('Username field blurred')}
        />
      </div>
      
      <div>
        <label htmlFor="email">Email:</label>
        <input
          id="email"
          name="email"
          type="email"
          value={formData.email}
          onChange={handleChange}
        />
      </div>
      
      <button type="submit">Submit</button>
    </form>
  );
}
      

Creating Event Handlers

Inline Event Handlers

The simplest approach is to define event handlers inline:


function InlineHandlerExample() {
  return (
    <button onClick={() => alert('Button clicked!')}>
      Click Me
    </button>
  );
}
      

Note: While inline handlers are simple, they can lead to unnecessary re-renders and make your JSX harder to read if the handler logic is complex.

Defined Function Handlers

For more complex handlers, define a function within your component:


function DefinedHandlerExample() {
  const handleClick = () => {
    // Complex logic here
    console.log('Button clicked!');
    // More operations...
  };
  
  return (
    <button onClick={handleClick}>
      Click Me
    </button>
  );
}
      

Event Handlers with Parameters

When you need to pass additional parameters to an event handler:


function ItemList() {
  const items = ['Apple', 'Banana', 'Cherry', 'Date'];
  
  const handleItemClick = (item, index, event) => {
    console.log(`Item clicked: ${item} at index ${index}`);
    console.log('Event:', event.type);
  };
  
  return (
    <ul>
      {items.map((item, index) => (
        <li 
          key={index}
          onClick={(e) => handleItemClick(item, index, e)}
        >
          {item}
        </li>
      ))}
    </ul>
  );
}
      
sequenceDiagram participant User participant Component participant Handler participant State User->>Component: Click Element Component->>Handler: Call Event Handler with Event Object Handler->>State: Update State if needed State->>Component: Re-render with new state

Event Handler Performance

Avoiding Unnecessary Re-renders

Every time a component re-renders, inline event handlers are recreated, which can cause performance issues in certain scenarios:

❌ Less Efficient


function ChildComponent({ onClick }) {
  console.log('Child rendered');
  return <button onClick={onClick}>Click</button>;
}

function ParentComponent() {
  const [count, setCount] = useState(0);
  
  // This creates a new function on every render
  return (
    <div>
      <p>Count: {count}</p>
      <ChildComponent 
        onClick={() => setCount(count + 1)} 
      />
    </div>
  );
}
          

The child component will re-render even if just the parent renders.

✅ More Efficient


function ChildComponent({ onClick }) {
  console.log('Child rendered');
  return <button onClick={onClick}>Click</button>;
}

// Optimize with React.memo
const MemoizedChild = React.memo(ChildComponent);

function ParentComponent() {
  const [count, setCount] = useState(0);
  
  // Use useCallback to memoize the function
  const handleClick = useCallback(() => {
    setCount(prevCount => prevCount + 1);
  }, []);
  
  return (
    <div>
      <p>Count: {count}</p>
      <MemoizedChild onClick={handleClick} />
    </div>
  );
}
          

Now the child component only re-renders when its props actually change.

The useCallback Hook

useCallback helps memoize event handler functions so they don't get recreated on every render:


import React, { useState, useCallback } from 'react';

function SearchComponent() {
  const [searchTerm, setSearchTerm] = useState('');
  const [results, setResults] = useState([]);
  
  // This function is memoized and only recreated if dependencies change
  const handleSearch = useCallback((event) => {
    const value = event.target.value;
    setSearchTerm(value);
    
    // Fictional search function
    fetchResults(value).then(data => {
      setResults(data);
    });
  }, []); // Empty dependency array - function is created once
  
  return (
    <div>
      <input 
        type="text"
        value={searchTerm}
        onChange={handleSearch}
        placeholder="Search..."
      />
      
      <ul>
        {results.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}
      

Event Propagation in React

Bubbling and Capturing

Like in standard DOM, React events follow the bubbling and capturing phases:

graph TD A[Root Element] --> B[Parent Element] B --> C[Target Element] D[1. Capturing Phase ↓] E[2. Target Phase] F[3. Bubbling Phase ↑]

By default, React event handlers listen for the bubbling phase. To listen for events during the capturing phase, append "Capture" to the event name:


function PropagationDemo() {
  return (
    <div 
      onClick={() => console.log('Outer div clicked (bubble)')}
      onClickCapture={() => console.log('Outer div clicked (capture)')}
      style={{ padding: '20px', backgroundColor: '#f0f0f0' }}
    >
      <div 
        onClick={() => console.log('Inner div clicked (bubble)')}
        onClickCapture={() => console.log('Inner div clicked (capture)')}
        style={{ padding: '20px', backgroundColor: '#d0d0d0' }}
      >
        <button 
          onClick={() => console.log('Button clicked (bubble)')}
          onClickCapture={() => console.log('Button clicked (capture)')}
        >
          Click me
        </button>
      </div>
    </div>
  );
}

// Output when button is clicked:
// Outer div clicked (capture)
// Inner div clicked (capture)
// Button clicked (capture)
// Button clicked (bubble)
// Inner div clicked (bubble)
// Outer div clicked (bubble)
      

Stopping Propagation

Sometimes you want to prevent events from propagating to parent elements:


function StopPropagationDemo() {
  return (
    <div 
      onClick={() => console.log('Outer div clicked')}
      style={{ padding: '20px', backgroundColor: '#f0f0f0' }}
    >
      <div 
        onClick={() => console.log('Inner div clicked')}
        style={{ padding: '20px', backgroundColor: '#d0d0d0' }}
      >
        <button 
          onClick={(e) => {
            e.stopPropagation(); // Stop the event from bubbling up
            console.log('Button clicked');
          }}
        >
          Click me (stops propagation)
        </button>
      </div>
    </div>
  );
}

// Output when button is clicked:
// Button clicked
      

Preventing Default Behavior

To prevent a browser's default action (like form submission or link navigation):


function PreventDefaultDemo() {
  return (
    <div>
      <h3>Form with preventDefault</h3>
      <form
        onSubmit={(e) => {
          e.preventDefault(); // Prevents page refresh
          console.log('Form submitted without page refresh');
        }}
      >
        <input type="text" placeholder="Enter something" />
        <button type="submit">Submit Form</button>
      </form>
      
      <h3>Link with preventDefault</h3>
      <a 
        href="https://example.com"
        onClick={(e) => {
          e.preventDefault(); // Prevents navigation
          console.log('Link clicked but no navigation occurred');
        }}
      >
        Click me (won't navigate)
      </a>
    </div>
  );
}
      

Real-world example: Think of event propagation like how sound travels in a room. If someone speaks (the event), people nearby hear it first, and then people further away (bubbling). Stopping propagation is like soundproofing a room so sound doesn't travel outside of it.

Advanced Event Handling Patterns

Event Delegation

Event delegation is a pattern where you attach a single event listener to a parent element instead of individual listeners on many child elements. React's event system uses this internally for performance, but you can also leverage it in your code:


function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Learn React', completed: false },
    { id: 2, text: 'Build an app', completed: false },
    { id: 3, text: 'Deploy to production', completed: false }
  ]);
  
  // Single event handler for the entire list
  const handleItemClick = (e) => {
    // Find the closest li element
    const li = e.target.closest('li');
    if (!li) return;
    
    // Get the todo id from the data attribute
    const todoId = Number(li.dataset.id);
    
    // Toggle the completed status
    setTodos(todos.map(todo => 
      todo.id === todoId 
        ? { ...todo, completed: !todo.completed } 
        : todo
    ));
  };
  
  return (
    <ul onClick={handleItemClick}>
      {todos.map(todo => (
        <li 
          key={todo.id}
          data-id={todo.id} 
          style={{ 
            textDecoration: todo.completed ? 'line-through' : 'none',
            cursor: 'pointer'
          }}
        >
          {todo.text}
        </li>
      ))}
    </ul>
  );
}
      

Custom Event Handling Hooks

Create reusable custom hooks for common event handling patterns:


// Custom hook for handling hover state
function useHover() {
  const [isHovering, setIsHovering] = useState(false);
  
  const handleMouseEnter = () => setIsHovering(true);
  const handleMouseLeave = () => setIsHovering(false);
  
  // Return both the state and the event handlers
  return {
    isHovering,
    hoverProps: {
      onMouseEnter: handleMouseEnter,
      onMouseLeave: handleMouseLeave
    }
  };
}

// Custom hook for handling drag and drop
function useDrag() {
  const [isDragging, setIsDragging] = useState(false);
  const [position, setPosition] = useState({ x: 0, y: 0 });
  
  const handleDragStart = (e) => {
    setIsDragging(true);
    // Store initial position
    const startX = e.clientX - position.x;
    const startY = e.clientY - position.y;
    
    const handleDragMove = (moveEvent) => {
      setPosition({
        x: moveEvent.clientX - startX,
        y: moveEvent.clientY - startY
      });
    };
    
    const handleDragEnd = () => {
      setIsDragging(false);
      window.removeEventListener('mousemove', handleDragMove);
      window.removeEventListener('mouseup', handleDragEnd);
    };
    
    window.addEventListener('mousemove', handleDragMove);
    window.addEventListener('mouseup', handleDragEnd);
  };
  
  return {
    isDragging,
    position,
    dragProps: {
      onMouseDown: handleDragStart,
      style: {
        position: 'absolute',
        left: `${position.x}px`,
        top: `${position.y}px`,
        cursor: isDragging ? 'grabbing' : 'grab'
      }
    }
  };
}

// Usage example
function DraggableElement() {
  const { isDragging, dragProps } = useDrag();
  const { isHovering, hoverProps } = useHover();
  
  return (
    <div 
      {...dragProps}
      {...hoverProps}
      style={{
        ...dragProps.style,
        backgroundColor: isHovering ? 'lightblue' : 'lightgray',
        padding: '20px',
        width: '100px',
        height: '100px',
        borderRadius: '8px',
        boxShadow: isDragging 
          ? '0px 10px 20px rgba(0,0,0,0.2)' 
          : '0px 2px 5px rgba(0,0,0,0.1)'
      }}
    >
      {isDragging ? 'Dragging' : 'Drag me'}
    </div>
  );
}
      

Real-World Applications

Interactive Data Table


function DataTable() {
  const [data, setData] = useState([
    { id: 1, name: 'Alice', age: 29, role: 'Developer' },
    { id: 2, name: 'Bob', age: 35, role: 'Designer' },
    { id: 3, name: 'Charlie', age: 42, role: 'Manager' },
    { id: 4, name: 'Diana', age: 25, role: 'Tester' }
  ]);
  
  const [sortField, setSortField] = useState(null);
  const [sortDirection, setSortDirection] = useState('asc');
  const [selectedRow, setSelectedRow] = useState(null);
  
  const handleHeaderClick = (field) => {
    // If clicking the same field, toggle direction, otherwise sort ascending
    if (sortField === field) {
      setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
    } else {
      setSortField(field);
      setSortDirection('asc');
    }
  };
  
  const handleRowClick = (id) => {
    setSelectedRow(selectedRow === id ? null : id);
  };
  
  // Sort the data
  const sortedData = [...data].sort((a, b) => {
    if (!sortField) return 0;
    
    const comparison = a[sortField] > b[sortField] ? 1 : -1;
    return sortDirection === 'asc' ? comparison : -comparison;
  });
  
  return (
    <table>
      <thead>
        <tr>
          <th 
            onClick={() => handleHeaderClick('name')}
            style={{ cursor: 'pointer' }}
          >
            Name
            {sortField === 'name' && (
              sortDirection === 'asc' ? '▲' : '▼'
            )}
          </th>
          <th 
            onClick={() => handleHeaderClick('age')}
            style={{ cursor: 'pointer' }}
          >
            Age
            {sortField === 'age' && (
              sortDirection === 'asc' ? '▲' : '▼'
            )}
          </th>
          <th 
            onClick={() => handleHeaderClick('role')}
            style={{ cursor: 'pointer' }}
          >
            Role
            {sortField === 'role' && (
              sortDirection === 'asc' ? '▲' : '▼'
            )}
          </th>
        </tr>
      </thead>
      <tbody>
        {sortedData.map(row => (
          <tr 
            key={row.id}
            onClick={() => handleRowClick(row.id)}
            style={{
              backgroundColor: selectedRow === row.id ? '#e0f7fa' : 'transparent',
              cursor: 'pointer'
            }}
          >
            <td>{row.name}</td>
            <td>{row.age}</td>
            <td>{row.role}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}
      

Interactive Image Gallery


function ImageGallery() {
  const images = [
    { id: 1, src: '/images/image1.jpg', alt: 'Mountain landscape' },
    { id: 2, src: '/images/image2.jpg', alt: 'Beach sunset' },
    { id: 3, src: '/images/image3.jpg', alt: 'Forest path' },
    { id: 4, src: '/images/image4.jpg', alt: 'City skyline' }
  ];
  
  const [selectedImage, setSelectedImage] = useState(null);
  const [lightboxOpen, setLightboxOpen] = useState(false);
  
  const handleImageClick = (image) => {
    setSelectedImage(image);
    setLightboxOpen(true);
  };
  
  const handleLightboxClose = () => {
    setLightboxOpen(false);
  };
  
  const handlePrevImage = (e) => {
    e.stopPropagation(); // Prevent closing the lightbox
    const currentIndex = images.findIndex(img => img.id === selectedImage.id);
    const prevIndex = (currentIndex - 1 + images.length) % images.length;
    setSelectedImage(images[prevIndex]);
  };
  
  const handleNextImage = (e) => {
    e.stopPropagation(); // Prevent closing the lightbox
    const currentIndex = images.findIndex(img => img.id === selectedImage.id);
    const nextIndex = (currentIndex + 1) % images.length;
    setSelectedImage(images[nextIndex]);
  };
  
  // Handle keyboard navigation
  useEffect(() => {
    const handleKeydown = (e) => {
      if (!lightboxOpen) return;
      
      switch (e.key) {
        case 'ArrowLeft':
          handlePrevImage(e);
          break;
        case 'ArrowRight':
          handleNextImage(e);
          break;
        case 'Escape':
          handleLightboxClose();
          break;
        default:
          break;
      }
    };
    
    window.addEventListener('keydown', handleKeydown);
    return () => window.removeEventListener('keydown', handleKeydown);
  }, [lightboxOpen, selectedImage]);
  
  return (
    <div>
      <div className="gallery-grid">
        {images.map(image => (
          <div 
            key={image.id} 
            className="gallery-item"
            onClick={() => handleImageClick(image)}
          >
            <img src={image.src} alt={image.alt} />
          </div>
        ))}
      </div>
      
      {lightboxOpen && selectedImage && (
        <div 
          className="lightbox-overlay"
          onClick={handleLightboxClose}
        >
          <div className="lightbox-content">
            <img src={selectedImage.src} alt={selectedImage.alt} />
            <button 
              className="lightbox-prev"
              onClick={handlePrevImage}
            >
              ←
            </button>
            <button 
              className="lightbox-next"
              onClick={handleNextImage}
            >
              →
            </button>
            <button 
              className="lightbox-close"
              onClick={handleLightboxClose}
            >
              ×
            </button>
          </div>
        </div>
      )}
    </div>
  );
}
      

Handling Events in Class Components

Although functional components are now the recommended approach, it's useful to understand how event handling works in class components:


class ButtonCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
    
    // Binding is necessary to make 'this' work in the callback
    this.handleClick = this.handleClick.bind(this);
  }
  
  handleClick() {
    this.setState(prevState => ({
      count: prevState.count + 1
    }));
  }
  
  // Alternative method using arrow function (no binding needed)
  handleReset = () => {
    this.setState({ count: 0 });
  };
  
  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.handleClick}>
          Increment
        </button>
        <button onClick={this.handleReset}>
          Reset
        </button>
      </div>
    );
  }
}
      

Key differences from functional components:

Practice Exercises

Exercise 1: Click Counter

Create a component with multiple buttons that:

Exercise 2: Color Picker

Build a color picker component that:

Exercise 3: Drag and Drop List

Create a sortable list component that:

Summary

In this lecture, we've covered:

Understanding React's event handling system is crucial for building interactive and responsive applications. It allows you to create seamless user experiences while leveraging React's performance optimizations.

Further Resources