AgentSkillsCN

event-delegation

在 React 中实现事件委托模式的指南,旨在减少属性钻取、提升性能,并有效应对动态或频繁变化的内容。附带示例、最佳实践、常见陷阱及实施 checklist。

SKILL.md
--- frontmatter
name: event-delegation
description: Guidelines for implementing the Event Delegation pattern in React to reduce prop drilling, improve performance, and handle dynamic or frequently changing content. Includes examples, best practices, common pitfalls, and an implementation checklist.

Event Delegation Pattern in React

Overview

Event delegation is a pattern where instead of attaching event handlers to individual child elements, you attach a single handler to a parent container. This eliminates the need to pass event handlers through props of all nested components.

When to use this skill

  • Use when you render large lists or grids with many interactive elements.
  • Use when you want to avoid prop drilling through deeply nested components.
  • Use when content is dynamic (added or removed at runtime) and you need handlers to work without reattaching.
  • Use when multiple similar elements share common behavior (for example: edit/delete/share actions per item).
  • Use when you need a single point of control for consistent logging, analytics, or access control.
  • Use when you want to reduce the number of event listeners for performance-sensitive UIs.

Core Principle

Instead of passing handlers through multiple levels of component props, attach a single event listener to a parent container and use event.target to identify which child element triggered the event.

Basic Implementation

Without Event Delegation (Prop Drilling)

code
const Parent = () => {
  const handleClick = (id: string) => {
    console.log(id);
  };

  return <Middle onItemClick={handleClick} />;
};

const Middle = ({ onItemClick }: TMiddleProps) => {
  return <Child onItemClick={onItemClick} />;
};

const Child = ({ onItemClick }: TChildProps) => {
  return <button onClick={() => onItemClick('item-1')}>Click</button>;
};

With Event Delegation

code
const Parent = () => {
  const handleContainerClick = (event: React.MouseEvent<HTMLDivElement>) => {
    const target = event.target as HTMLElement;
    const button = target.closest('[data-action]');

    if (button) {
      const action = button.getAttribute('data-action');
      const id = button.getAttribute('data-id');
      console.log(action, id);
    }
  };

  return (
    <div onClick={handleContainerClick}>
      <Middle />
    </div>
  );
};

const Middle = () => {
  return <Child />;
};

const Child = () => {
  return (
    <button data-action="click" data-id="item-1">
      Click
    </button>
  );
};

Use Cases

1. Lists with Multiple Actions

When rendering lists where each item has multiple interactive elements.

code
const ItemList = () => {
  const { items, handleAction } = useItemList();

  const handleDelegatedClick = (event: React.MouseEvent<HTMLDivElement>) => {
    const target = event.target as HTMLElement;
    const actionElement = target.closest('[data-action]');

    if (!actionElement) return;

    const action = actionElement.getAttribute('data-action');
    const itemId = actionElement.getAttribute('data-item-id');

    if (action && itemId) {
      handleAction(action, itemId);
    }
  };

  return (
    <div onClick={handleDelegatedClick}>
      {items.map(item => (
        <ListItem key={item.id} item={item} />
      ))}
    </div>
  );
};

const ListItem = ({ item }: TListItemProps) => {
  return (
    <div>
      <button data-action="edit" data-item-id={item.id}>
        Edit
      </button>
      <button data-action="delete" data-item-id={item.id}>
        Delete
      </button>
      <button data-action="share" data-item-id={item.id}>
        Share
      </button>
    </div>
  );
};

2. Dynamic Content

When content is added or removed dynamically without re-rendering.

code
const DynamicContainer = () => {
  const handleDelegatedEvent = (event: React.MouseEvent<HTMLDivElement>) => {
    const target = event.target as HTMLElement;
    const clickable = target.closest('[data-clickable]');

    if (clickable) {
      const elementId = clickable.getAttribute('data-element-id');
      console.log('Clicked element:', elementId);
    }
  };

  return (
    <div onClick={handleDelegatedEvent}>
      <DynamicContent />
    </div>
  );
};

3. Forms with Multiple Inputs

When handling multiple form inputs without individual handlers.

code
const FormContainer = () => {
  const { formData, updateField } = useFormData();

  const handleInputChange = (event: React.ChangeEvent<HTMLDivElement>) => {
    const target = event.target as HTMLInputElement;

    if (target.hasAttribute('data-field')) {
      const field = target.getAttribute('data-field');
      if (field) {
        updateField(field, target.value);
      }
    }
  };

  return (
    <div onChange={handleInputChange}>
      <FormFields />
    </div>
  );
};

const FormFields = () => {
  return (
    <>
      <input data-field="name" type="text" />
      <input data-field="email" type="email" />
      <input data-field="phone" type="tel" />
    </>
  );
};

4. Nested Navigation Menus

When building complex navigation structures.

code
const Navigation = () => {
  const { navigate } = useNavigation();

  const handleNavigationClick = (event: React.MouseEvent<HTMLElement>) => {
    const target = event.target as HTMLElement;
    const link = target.closest('[data-route]');

    if (link) {
      event.preventDefault();
      const route = link.getAttribute('data-route');
      if (route) {
        navigate(route);
      }
    }
  };

  return (
    <nav onClick={handleNavigationClick}>
      <MenuLevel1 />
    </nav>
  );
};

const MenuLevel1 = () => {
  return (
    <ul>
      <li>
        <a data-route="/home">Home</a>
        <MenuLevel2 />
      </li>
    </ul>
  );
};

const MenuLevel2 = () => {
  return (
    <ul>
      <li><a data-route="/about">About</a></li>
      <li><a data-route="/contact">Contact</a></li>
    </ul>
  );
};

5. Cards with Multiple Interactive Zones

When card components have multiple clickable areas with different actions.

code
const CardGrid = () => {
  const handleCardInteraction = (event: React.MouseEvent<HTMLDivElement>) => {
    const target = event.target as HTMLElement;
    const actionElement = target.closest('[data-card-action]');

    if (actionElement) {
      const action = actionElement.getAttribute('data-card-action');
      const cardId = actionElement.getAttribute('data-card-id');

      if (action === 'view') {
        console.log('View card:', cardId);
      } else if (action === 'favorite') {
        console.log('Favorite card:', cardId);
      } else if (action === 'settings') {
        console.log('Settings for card:', cardId);
      }
    }
  };

  return (
    <div onClick={handleCardInteraction}>
      <Card id="1" />
      <Card id="2" />
      <Card id="3" />
    </div>
  );
};

const Card = ({ id }: TCardProps) => {
  return (
    <div data-card-action="view" data-card-id={id}>
      <CardHeader id={id} />
      <CardBody />
      <CardFooter id={id} />
    </div>
  );
};

const CardHeader = ({ id }: TCardHeaderProps) => {
  return (
    <div>
      <button
        data-card-action="favorite"
        data-card-id={id}
        onClick={(e) => e.stopPropagation()}
      >
        ★
      </button>
    </div>
  );
};

const CardFooter = ({ id }: TCardFooterProps) => {
  return (
    <button
      data-card-action="settings"
      data-card-id={id}
      onClick={(e) => e.stopPropagation()}
    >
      Settings
    </button>
  );
};

Advanced Patterns

Multiple Event Types

code
const MultiEventContainer = () => {
  const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
    const target = event.target as HTMLElement;
    const element = target.closest('[data-click-action]');

    if (element) {
      const action = element.getAttribute('data-click-action');
      console.log('Click action:', action);
    }
  };

  const handleDoubleClick = (event: React.MouseEvent<HTMLDivElement>) => {
    const target = event.target as HTMLElement;
    const element = target.closest('[data-dblclick-action]');

    if (element) {
      const action = element.getAttribute('data-dblclick-action');
      console.log('Double click action:', action);
    }
  };

  const handleContextMenu = (event: React.MouseEvent<HTMLDivElement>) => {
    const target = event.target as HTMLElement;
    const element = target.closest('[data-context-action]');

    if (element) {
      event.preventDefault();
      const action = element.getAttribute('data-context-action');
      console.log('Context menu action:', action);
    }
  };

  return (
    <div
      onClick={handleClick}
      onDoubleClick={handleDoubleClick}
      onContextMenu={handleContextMenu}
    >
      <Content />
    </div>
  );
};

Event Delegation with State Management

code
const DataTable = () => {
  const {
    rows,
    selectedRows,
    sortColumn,
    handleRowSelect,
    handleSort,
    handleRowAction
  } = useDataTable();

  const handleTableClick = (event: React.MouseEvent<HTMLTableElement>) => {
    const target = event.target as HTMLElement;

    const checkbox = target.closest('[data-select]');
    if (checkbox) {
      const rowId = checkbox.getAttribute('data-row-id');
      if (rowId) {
        handleRowSelect(rowId);
        return;
      }
    }

    const sortHeader = target.closest('[data-sort]');
    if (sortHeader) {
      const column = sortHeader.getAttribute('data-column');
      if (column) {
        handleSort(column);
        return;
      }
    }

    const actionButton = target.closest('[data-row-action]');
    if (actionButton) {
      const action = actionButton.getAttribute('data-row-action');
      const rowId = actionButton.getAttribute('data-row-id');
      if (action && rowId) {
        handleRowAction(action, rowId);
      }
    }
  };

  return (
    <table onClick={handleTableClick}>
      <TableHeader sortColumn={sortColumn} />
      <tbody>
        {rows.map(row => (
          <TableRow
            key={row.id}
            row={row}
            isSelected={selectedRows.includes(row.id)}
          />
        ))}
      </tbody>
    </table>
  );
};

const TableHeader = ({ sortColumn }: TTableHeaderProps) => {
  return (
    <thead>
      <tr>
        <th data-sort data-column="name">Name</th>
        <th data-sort data-column="date">Date</th>
        <th data-sort data-column="status">Status</th>
      </tr>
    </thead>
  );
};

const TableRow = ({ row, isSelected }: TTableRowProps) => {
  return (
    <tr>
      <td>
        <input
          type="checkbox"
          data-select
          data-row-id={row.id}
          checked={isSelected}
          readOnly
        />
      </td>
      <td>{row.name}</td>
      <td>{row.date}</td>
      <td>
        <button data-row-action="edit" data-row-id={row.id}>Edit</button>
        <button data-row-action="delete" data-row-id={row.id}>Delete</button>
      </td>
    </tr>
  );
};

Best Practices

1. Use Data Attributes

Always use data attributes to identify actionable elements and carry metadata.

code
<button data-action="submit" data-form-id="user-form">Submit</button>

2. Use closest() Method

Use closest() to find the target element even if the click occurred on a child element.

code
const button = (event.target as HTMLElement).closest('[data-action]');

3. Stop Propagation When Needed

Prevent event bubbling for nested interactive elements.

code
<button onClick={(e) => e.stopPropagation()}>Nested Action</button>

4. Type Safety

Always cast event targets and check for null values.

code
const target = event.target as HTMLElement;
const element = target.closest('[data-action]');

if (element) {
  const action = element.getAttribute('data-action');
  if (action) {
    handleAction(action);
  }
}

5. Performance Consideration

Event delegation reduces the number of event listeners but adds a small overhead for event matching. Use it when you have many similar elements.

When to Use Event Delegation

Use when:

  • You have deeply nested component trees
  • You render large lists with interactive elements
  • You have dynamic content that changes frequently
  • You want to avoid prop drilling
  • You have multiple similar elements with similar handlers

Avoid when:

  • You need complex event-specific logic for each element
  • You need to prevent default behavior selectively
  • Performance of event matching becomes a bottleneck
  • Component tree is shallow and props are easy to pass

Common Pitfalls

1. Forgetting to Stop Propagation

When you have nested clickable elements, remember to stop propagation to prevent triggering multiple handlers.

code
<div data-action="parent-action">
  <button
    data-action="child-action"
    onClick={(e) => e.stopPropagation()}
  >
    Child
  </button>
</div>

2. Not Checking for Null

Always check if closest() or getAttribute() returns a value.

code
const element = target.closest('[data-action]');
if (!element) return;

const action = element.getAttribute('data-action');
if (!action) return;

3. Using Event Delegation for Everything

Don't overuse event delegation. For simple cases with few components, direct props are clearer.

Implementation Checklist

  • Identify the common parent container
  • Add single event handler to parent
  • Add data attributes to child elements
  • Use closest() to find target elements
  • Extract and validate data attributes
  • Handle null cases properly
  • Add stopPropagation() where needed
  • Test with nested interactive elements