How can I shorten or optimize event handlers in React?
I’m developing a component with multiple event handlers that have similar structure. Here’s an example of the code:
const [selectedValue, setSelectedValue] = React.useState(1);
const [selectedColorValue, setSelectedColorValue] = React.useState("black");
const handleSelectChange = (event) => {
setSelectedValue(event.target.value);
};
const handleSelectColorChange = (event) => {
setSelectedColorValue(event.target.value);
};
Is it possible to create a universal handler or use other approaches to reduce code duplication? Should I optimize such code early in development or leave it as is until refactoring becomes necessary?
Main approach to reducing code duplication in React event handlers
The main approach to reducing code duplication in React event handlers involves creating a universal handler using useCallback for performance optimization. You can create a single handler that accepts a parameter to determine which state to update, which significantly reduces the amount of duplicated code and improves component performance.
Table of Contents
- Optimizing Event Handlers in React
- Creating a Universal Handler
- Using useCallback for Performance
- Other Optimization Methods
- When to Start Optimizing
- Practical Examples and Patterns
- Conclusion
Optimizing Event Handlers in React
Event handlers are one of the main sources of performance issues in React applications. When a component re-renders, handler functions are recreated even if their logic hasn’t changed. This leads to unnecessary re-renders of child components, especially when using optimized components like React.memo or PureComponent source.
Main performance issues:
- Creating new function instances on every render
- Passing new handler references to child components
- Needing to use complex techniques to prevent memory leaks
There are several approaches to optimization, from simple handler combination to advanced patterns with hooks.
Creating a Universal Handler
In your case with two similar handlers, you can create a universal handler that accepts a parameter to determine which state to update. This will not only reduce code but also make it more scalable.
const [selectedValue, setSelectedValue] = React.useState(1);
const [selectedColorValue, setSelectedColorValue] = React.useState("black");
const handleUniversalChange = React.useCallback((setter, event) => {
setter(event.target.value);
}, []);
// Usage in components
<select onChange={(e) => handleUniversalChange(setSelectedValue, e)}>
<option value="1">Option 1</option>
<option value="2">Option 2</option>
</select>
<select onChange={(e) => handleUniversalChange(setSelectedColorValue, e)}>
<option value="black">Black</option>
<option value="white">White</option>
</select>
This approach allows you to:
- Reduce the amount of duplicated code
- Provide a single pattern for all change handlers
- Easily add new fields in the future
An alternative is to create an object of handlers:
const handlers = {
selectedValue: setSelectedValue,
selectedColorValue: setSelectedColorValue
};
const handleChange = React.useCallback((key) => (event) => {
handlers[key](event.target.value);
}, []);
Using useCallback for Performance
useCallback is a React hook that memoizes functions, preserving them between renders. This is critical for optimizing performance when passing handlers to child components source.
const [selectedValue, setSelectedValue] = React.useState(1);
const [selectedColorValue, setSelectedColorValue] = React.useState("black");
const handleSelectChange = React.useCallback((event) => {
setSelectedValue(event.target.value);
}, []);
const handleSelectColorChange = React.useCallback((event) => {
setSelectedColorValue(event.target.value);
}, []);
// Or for the universal handler
const handleUniversalChange = React.useCallback((setter) => (event) => {
setter(event.target.value);
}, []);
Why useCallback is important:
- Prevents unnecessary re-renders of child components
- Maintains a stable function reference between renders
- Works better with
React.memoandPureComponentsource
When working with large lists of elements, useCallback becomes especially important:
const handleItemClick = React.useCallback((itemId) => {
setSelectedItems(prev => [...prev, itemId]);
}, []);
Other Optimization Methods
1. Debouncing for Input Fields
For input fields where users type quickly, you can use debouncing to optimize performance source:
const debounce = (func, delay) => {
let debounceTimer;
return function() {
const context = this;
const args = arguments;
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => func.apply(context, args), delay);
}
};
const handleInputChange = debounce((event) => {
setSearchTerm(event.target.value);
}, 300);
2. Handling Multiple Events
If elements have similar handlers, you can use a single handler for multiple events source:
const handleEvent = React.useCallback((eventType) => (event) => {
switch(eventType) {
case 'click':
handleClick(event);
break;
case 'change':
handleChange(event);
break;
// other cases
}
}, []);
3. Creating Custom Hooks
For complex scenarios, you can create custom hooks that encapsulate handler logic source:
function useInput(initialValue) {
const [value, setValue] = useState(initialValue);
return {
value,
onChange: useCallback((event) => setValue(event.target.value), []),
reset: useCallback(() => setValue(initialValue), [initialValue])
};
}
// Usage
const { value: selectedValue, onChange: handleSelectChange } = useInput(1);
const { value: selectedColorValue, onChange: handleColorChange } = useInput("black");
When to Start Optimizing
Early Development Stages
Optimize early when:
- The component has multiple similar handlers
- You plan to use
React.memoorPureComponent - The component will pass handlers to child elements
- Code has already started duplicating in multiple places
Early optimization allows you to:
- Create consistent patterns for the team
- Avoid future refactoring
- Improve performance from the start
Later Development Stages
You can postpone optimization if:
- The component is simple and has no performance issues
- Code isn’t duplicated and is easy to read
- The team is focused on functionality rather than optimization
- The application is in development and architecture may change
Practical Examples and Patterns
Example 1: Universal Handler for Forms
function useForm(initialValues) {
const [values, setValues] = useState(initialValues);
const handleChange = useCallback((name) => (event) => {
setValues(prev => ({
...prev,
[name]: event.target.value
}));
}, []);
const resetForm = useCallback(() => {
setValues(initialValues);
}, [initialValues]);
return {
values,
handleChange,
resetForm
};
}
// Usage
const { values, handleChange, resetForm } = useForm({
selectedValue: 1,
selectedColorValue: "black"
});
// In JSX
<input
name="selectedValue"
value={values.selectedValue}
onChange={handleChange('selectedValue')}
/>
Example 2: Optimization for Large Lists
const ItemList = React.memo(({ items, onItemClick }) => {
return (
<div>
{items.map(item => (
<div
key={item.id}
onClick={() => onItemClick(item.id)}
style={{ cursor: 'pointer' }}
>
{item.name}
</div>
))}
</div>
);
});
const ParentComponent = ({ availableItems }) => {
const [selectedItems, setSelectedItems] = useState([]);
const handleItemClick = useCallback((itemId) => {
setSelectedItems(prev => [...prev, itemId]);
}, []);
return (
<ItemList
items={availableItems}
onItemClick={handleItemClick}
/>
);
};
Example 3: Combined Approach
function useEventHandlers() {
const [state, setState] = useState({
selectedValue: 1,
selectedColorValue: "black"
});
const handleChange = useCallback((field) => (event) => {
setState(prev => ({
...prev,
[field]: event.target.value
}));
}, []);
return {
state,
handleChange
};
}
// Usage
const { state, handleChange } = useEventHandlers();
// In JSX
<select
value={state.selectedValue}
onChange={handleChange('selectedValue')}
>
<option value="1">Option 1</option>
<option value="2">Option 2</option>
</select>
<select
value={state.selectedColorValue}
onChange={handleChange('selectedColorValue')}
>
<option value="black">Black</option>
<option value="white">White</option>
</select>
Conclusion
Optimizing event handlers in React is an important practice for creating performant and maintainable applications. Key takeaways:
-
Universal handlers significantly reduce code duplication and create consistent patterns for form components.
-
useCallback is an essential tool for preventing unnecessary re-renders and maintaining stable function references between renders.
-
Custom hooks encapsulate complex logic and ensure code reusability, which is especially useful for large projects.
-
Optimization timing depends on context: for components with multiple handlers and complex logic, optimization is needed early on; for simple components, it can be postponed until necessary.
-
Combined approaches that combine universal handlers, useCallback, and custom hooks provide maximum performance and code cleanliness.
It’s recommended to start with simple universal handlers and gradually implement more complex patterns as the application grows and components become more complex.
Sources
- Optimizing React component event handlers - Nicholas Tsim
- Optimizing Event Handlers in React using useCallback - DEV Community
- JavaScript Event Listeners: How to Handle Multiple Events - The Daily Frontend
- Optimizing event handler of input without affecting browser and application performance in React.js - Medium
- React Performance: Event Handlers using
useCallbackhook - Medium - Demystifying useCallback in React - Medium
- React useCallback: When and how to use it for better performance - LogRocket
- Advanced React useCallback patterns that actually improve performance - Medium
- React ‘useCallback’ for event handler - Stack Overflow
- When to use useCallback — Antoine Quinquenel