How can I optimize and reduce duplicate event handlers in React?
During the development of a React component, I’ve encountered a need for multiple event handlers with similar structure. The code below demonstrates two handlers that are practically identical in logic but work with different states:
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 that works with different states, or are there other approaches to reducing this kind of code duplication? Which method would be most efficient in terms of performance and long-term code maintainability?
Yes, you can create a universal handler that works with different states using several approaches. The most effective methods are using a single state object with a dynamic updating handler, or applying a parameterized handler with useCallback for performance optimization.
Contents
- Creating a Universal Event Handler
- Using a Single State Object
- Parameterized Handlers
- Optimization with useCallback
- Performance and Maintainability Comparison
- Practical Implementation Examples
Creating a Universal Event Handler
The most common approach to eliminate code duplication is creating a universal handler that can work with any state. Here’s how you can implement this in your case:
const [selectedValue, setSelectedValue] = React.useState(1);
const [selectedColorValue, setSelectedColorValue] = React.useState("black");
const handleGenericChange = (setState) => (event) => {
setState(event.target.value);
};
// Usage
<select value={selectedValue} onChange={handleGenericChange(setSelectedValue)}>
<option value={1}>Option 1</option>
<option value={2}>Option 2</option>
</select>
<select value={selectedColorValue} onChange={handleGenericChange(setSelectedColorValue)}>
<option value="black">Black</option>
<option value="white">White</option>
</select>
This approach allows you to use the same pattern for any state by passing the required setState function as an argument.
Using a Single State Object
A more modern and often recommended approach is to combine related states into a single object. This allows you to use a single handler for all fields:
const [formData, setFormData] = React.useState({
selectedValue: 1,
selectedColorValue: "black"
});
const handleFormChange = (event) => {
const { name, value } = event.target;
setFormData(prevState => ({
...prevState,
[name]: value
}));
};
// In JSX, you need to specify name for each element
<select
name="selectedValue"
value={formData.selectedValue}
onChange={handleFormChange}
>
<option value={1}>Option 1</option>
<option value={2}>Option 2</option>
</select>
<select
name="selectedColorValue"
value={formData.selectedColorValue}
onChange={handleFormChange}
>
<option value="black">Black</option>
<option value="white">White</option>
</select>
This approach is preferred because:
- Reduces the number of state variables
- Ensures atomic updates of multiple related states
- Simplifies data passing between components
- Easier to extend with new fields
Parameterized Handlers
Another approach is using a handler with parameters that can be partially applied:
const handleStateChange = (setState) => (event) => {
setState(event.target.value);
};
// Or a more flexible version
const handleChange = (key, setState) => (event) => {
setState(prevState => ({
...prevState,
[key]: event.target.value
}));
};
// Usage
<select
value={selectedValue}
onChange={handleChange('selectedValue', setSelectedValue)}
>
This approach is particularly useful when you have many similar states that aren’t logically connected in a single object.
Optimization with useCallback
When using universal handlers, it’s important to consider performance. Each time a component renders, new functions are created, which can lead to unnecessary re-renders of child components.
Use useCallback to memoize handlers:
import React, { useState, useCallback } from 'react';
const [selectedValue, setSelectedValue] = useState(1);
const [selectedColorValue, setSelectedColorValue] = useState("black");
const handleGenericChange = useCallback((setState) => (event) => {
setState(event.target.value);
}, []); // Empty array means the function won't be recreated
// For the single form data handler
const handleFormChange = useCallback((event) => {
const { name, value } = event.target;
setFormData(prevState => ({
...prevState,
[name]: value
}));
}, []); // Dependencies aren't needed here since we're using a callback function
Important: As noted in performance research,
useCallbackwith an empty dependency array can cause the handler to never update. Make sure this is safe for your specific use case.
Performance and Maintainability Comparison
Performance
| Approach | Performance | Recommendation |
|---|---|---|
| Individual handlers | Most efficient for isolated states | Good for small components |
| Universal handler | Requires optimization with useCallback |
Moderate performance |
| Single state object | Best balance of performance and maintainability | Recommended for forms and related data |
Maintainability
-
Single state object - most maintainable approach because:
- Easy to add new fields
- Convenient validation of all data
- Simple submission to server
- Easier to implement change rollback
-
Universal handlers - good for components with the same structure but different states
-
Individual handlers - acceptable only for very simple cases with 1-2 states
Practical Implementation Examples
Example 1: Form with multiple fields
import React, { useState, useCallback } from 'react';
function UserForm() {
const [user, setUser] = useState({
name: '',
email: '',
age: '',
city: ''
});
const handleChange = useCallback((event) => {
const { name, value } = event.target;
setUser(prevUser => ({
...prevUser,
[name]: value
}));
}, []);
const handleSubmit = useCallback((event) => {
event.preventDefault();
console.log('Submitting data:', user);
}, [user]);
return (
<form onSubmit={handleSubmit}>
<input
type="text"
name="name"
value={user.name}
onChange={handleChange}
placeholder="Name"
/>
<input
type="email"
name="email"
value={user.email}
onChange={handleChange}
placeholder="Email"
/>
<input
type="number"
name="age"
value={user.age}
onChange={handleChange}
placeholder="Age"
/>
<button type="submit">Submit</button>
</form>
);
}
Example 2: Dynamic list with universal handlers
import React, { useState, useCallback } from 'react';
function DynamicList() {
const [items, setItems] = useState([
{ id: 1, name: 'Item 1', value: '' },
{ id: 2, name: 'Item 2', value: '' }
]);
const handleItemChange = useCallback((id) => (event) => {
setItems(prevItems =>
prevItems.map(item =>
item.id === id
? { ...item, value: event.target.value }
: item
)
);
}, []);
return (
<div>
{items.map(item => (
<div key={item.id}>
<label>{item.name}</label>
<input
type="text"
value={item.value}
onChange={handleItemChange(item.id)}
/>
</div>
))}
</div>
);
}
Sources
- React interactivity: Events and state - MDN
- How to use same state for multiple controlled components in react hooks - DEV Community
- How to properly optimize event handlers in react - Stack Overflow
- Performance penalty of creating handlers on every render with react-hooks - Stack Overflow
- Optimizing React component event handlers - Nicholas Tsim
- React useState hook event handler using initial state - Stack Overflow
- useState – React
Conclusion
To optimize and reduce duplicate event handlers in React, the following approaches are recommended:
- Single state object - the most preferred method for forms and related data, providing better performance and maintainability
- Universal handlers with useCallback - effective for components with the same structure but require performance optimization
- Parameterized handlers - flexible approach for dynamic lists and repetitive components
The choice of a specific method depends on your application context, but in most cases, combining related states into a single object with a single handler is most efficient in terms of both performance and long-term code maintainability.