Introduction
Beyond the basics of attaching event handlers, React applications use several established patterns for managing events at scale. These patterns handle common scenarios like passing data to handlers, conditionally handling events, building event-driven communication between components, and managing complex keyboard interactions. Mastering these patterns will make your components more reusable and your event handling code more maintainable.
Key Concepts
- Event handler props: Passing event handler functions as props to child components, following the
onXxxnaming convention. - Event delegation in lists: Using a single handler on a parent element to manage events from many child elements.
- Keyboard navigation: Building accessible keyboard interactions with
onKeyDownand focus management. - Composed handlers: Creating higher-order functions that combine multiple event behaviors.
Real World Context
Consider a data table component with sortable columns, selectable rows, editable cells, and keyboard navigation. Each cell might need click, double-click, key press, and focus handlers. Without patterns for organizing this complexity, the code quickly becomes unmanageable. Event handler props let parent components control behavior, delegation reduces the number of listeners, and composed handlers keep individual handler functions focused on one responsibility.
Deep Dive
Pattern 1: Event handler props on custom components
tsx// The component exposes an onXxx prop function SearchInput({ onSearch }: { onSearch: (query: string) => void }) { const [query, setQuery] = useState(""); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); onSearch(query); }; return ( <form onSubmit={handleSubmit}> <input value={query} onChange={(e) => setQuery(e.target.value)} /> <button type="submit">Search</button> </form> ); } // Parent controls what happens function App() { return <SearchInput onSearch={(q) => console.log("Searching:", q)} />; }
Pattern 2: Composed event handlers
tsxfunction composeHandlers<E>( ...handlers: Array<((e: E) => void) | undefined> ) { return (e: E) => { handlers.forEach((handler) => handler?.(e)); }; } // Usage: run both the internal and external handlers function Input({ onChange, ...props }: React.InputHTMLAttributes<HTMLInputElement>) { const internalChange = (e: React.ChangeEvent<HTMLInputElement>) => { analytics.track("input_change", { value: e.target.value }); }; return ( <input onChange={composeHandlers(internalChange, onChange)} {...props} /> ); }
Pattern 3: Keyboard navigation
tsxfunction Menu({ items }: { items: string[] }) { const [activeIndex, setActiveIndex] = useState(0); const handleKeyDown = (e: React.KeyboardEvent) => { switch (e.key) { case "ArrowDown": e.preventDefault(); setActiveIndex((i) => Math.min(i + 1, items.length - 1)); break; case "ArrowUp": e.preventDefault(); setActiveIndex((i) => Math.max(i - 1, 0)); break; case "Enter": console.log("Selected:", items[activeIndex]); break; } }; return ( <ul role="listbox" tabIndex={0} onKeyDown={handleKeyDown}> {items.map((item, i) => ( <li key={i} role="option" aria-selected={i === activeIndex} className={i === activeIndex ? "active" : ""}> {item} </li> ))} </ul> ); }
Common Pitfalls
- Creating new arrow functions in render for lists — Writing
onClick={() => handle(id)}inside a large list creates a new function per item per render. For most cases this is fine, but in very large lists (1000+ items), consider event delegation on the parent. - Forgetting
e.preventDefault()on keyboard handlers — Arrow keys and Space scroll the page by default. Calle.preventDefault()in keyboard handlers to prevent the browser's default scrolling behavior.
Best Practices
- Follow the
onXxx/handleXxxnaming convention — Name propsonSearch,onSelect,onDeleteand internal handlershandleSearch,handleSelect,handleDelete. This makes the data flow immediately clear. - Add keyboard support for accessibility — Any interactive element that uses
onClickshould also supportonKeyDownfor Enter and Space keys, ensuring keyboard users can interact with your UI.
Summary
- Use
onXxxprop naming for event handler props passed to child components, withhandleXxxfor internal handlers. - Composed handlers let you combine multiple behaviors (analytics, validation, external callbacks) into a single event handler.
- Keyboard navigation patterns with
onKeyDownare essential for building accessible interactive components.
Code Examples
// Event handler prop pattern
function ToggleButton({ onToggle, active }: {
onToggle: (newState: boolean) => void;
active: boolean;
}) {
return (
<button
onClick={() => onToggle(!active)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onToggle(!active);
}
}}
role="switch"
aria-checked={active}
>
{active ? "ON" : "OFF"}
</button>
);
}
function App() {
const [darkMode, setDarkMode] = useState(false);
return <ToggleButton active={darkMode} onToggle={setDarkMode} />;
}