React Hook Tip: Handling Events Directly for Cleaner Code
When starting out with React, the useEffect hook is often used for various side effects, like data fetching, subscription, or modifying the DOM. But what if I told you that using useEffect
for handling state updates might not always be the best approach? In fact, you can make your React code simpler, more readable, and even avoid some unnecessary re-renders by handling events directly.
Let's talk about a common pattern that beginners encounter. Take a chat component, for example. Whenever a new message is added, you want to update the state of your messages and persist them somewhere, like in local storage.
Here’s one way to handle this (the indirect way):
function ChatView() {
const [messages, setMessages] = useState([]);
useEffect(() => {
persistMessages(messages);
}, [messages]);
return (
<div>
<NewMessage
onSubmit={(message) => {
setMessages([...messages, message]);
}}
/>
</div>
);
}
The above approach uses useEffect to trigger the persistMessages
function every time the messages
state changes. At first glance, this looks fine. However, it's indirect and can lead to confusion, especially as the component grows. Why? Because now you're thinking in two steps:
- First, set the messages.
- Then, whenever
messages
change, do something.
That can quickly become messy, as useEffect will trigger whenever any state or prop related to the messages
changes, even in ways you might not expect.
The Better Approach: Handle Events Directly
Instead of relying on useEffect
to manage changes to your state, you can handle everything in the exact moment the event happens. Here’s a cleaner, more direct version:
function ChatView() {
const [messages, setMessages] = useState([]);
function updateMessages(messages) {
setMessages(messages);
persistMessages(messages);
}
return (
<div>
<NewMessage
onSubmit={(message) => {
updateMessages([...messages, message]);
}}
/>
</div>
);
}
In this version, we create an explicit function that handles both setting the state and persisting the messages. Now, instead of waiting for useEffect to notice the change, you immediately take care of both actions—updating the messages and persisting them—right when the user submits a new message.
Why Is This Better?
- Clarity: You can easily trace what happens when an event occurs. When a new message is submitted, you directly update the state and persist it.
- Avoid Unnecessary Effects: You avoid extra work that useEffect might trigger unnecessarily, especially when changes in other parts of the component might affect
messages
. - Less Cognitive Overhead: You're not thinking about state changes triggering effects—you’re thinking about actions tied directly to user events.
Other Considerations
Avoid Overuse of useEffect
While useEffect
is a powerful tool in React, beginners often fall into the trap of overusing it. It’s tempting to think of useEffect as a "catch-all" for every time something changes. But sometimes, there are better ways to handle logic. Whenever possible, it's a good idea to place logic where it's directly tied to the event that causes it.
Keep Business Logic Outside of JSX
Another trap to avoid is cramming too much business logic inside JSX elements. For example, in the first code block, we have an inline function inside onSubmit
that updates the messages. That can make the component hard to read. In the second, we've extracted that logic into a reusable function (updateMessages
), making the JSX much cleaner.
Embrace Function Composition
Breaking your logic down into smaller, well-named functions (like updateMessages
) makes your code more maintainable and easier to test. If you ever need to change the way messages are updated or persisted, you now have a single place to do that.
Finally
When working with React, it’s tempting to use useEffect
for everything. But remember, not all state changes need to be handled reactively. Often, handling them directly during the event gives you cleaner and more predictable code.
If you're handling some logic based on a user action—like submitting a message—do it directly at the point of action rather than waiting for the state to change. This way, you keep your code simpler and more understandable, especially as your component grows.