Why You Should Be Cautious With The useEffect Hook In React.
What is Wrong With useEffect?
Let’s look at what this actually looks like in practice. Imagine you’re building a simple profile page where a user updates their profile name.
import React, { useState, useEffect } from 'react';function ProfileSettings() {
const [userData, setUserData] = useState({ name: 'Bahadur' });
const [triggerSave, setTriggerSave] = useState(false);
const handleSaveClick = () => {
// The button click handler does nothing but flip a switch
setTriggerSave(true);
};
useEffect(() => {
// The effect watches the switch
if (triggerSave) {
api.updateProfile(userData)
.then(() => {
alert('Saved!');
setTriggerSave(false); // Remember to turn the switch off!
})
.catch(() => setTriggerSave(false));
}
}, [triggerSave, userData]);
return ;
}
This code works, but it’s incredibly flawed.
Here, you have the managing of the switch. You have to remember to set triggerSave back to false inside both the .then() and the .catch(). If you miss a spot, the button clicks exactly once and then is useless , until the user refreshes the page.
Get Shreyash’s stories in your inbox
Join Medium for free to get updates from this writer.
Second, you’ve introduced the dependency array problem. Because the effect uses userData, we are forced to add it to the dependency array. If userData changes while triggerSave happens to be true, the effect runs again entirely on its own, sending false requests to your database.
Actions and Synchronization
To write React code which is stable, you have to separate user actions from component synchronization.
useEffectis for synchronization. It exists to keep your component in sync with a system outside of React’s immediate control. This means things like setting up a WebSocket connection, subscribing to events or synchronizing state with browser APIs likelocalStorage, or fetching initial data when a page first loads (any side-effects, which are not related to the component’s JSX code at all).- Event Handlers are for user actions. They run because a human being clicked a button, typed in an input, or submitted a form at a specific moment of time.
Saving a profile isn’t a synchronization issue. Your application doesn’t need to trigger a database update just because the component showed up on the screen. It needs to send that update because a user intentionally pressed “Save Changes.”
When you direct a user action through a state variable and into a useEffect, you are taking a single sequential event and splitting it across two completely different places in your file. It makes debugging feel like frustrating.
The Solution
The solution is incredibly liberating: Delete the useEffect entirely.
Plain old JavaScript functions are still the absolute best tool for handling user events. You can use async/await directly inside your event handlers without any side-effect occuring.
import React, { useState } from 'react';function ProfileSettings() {
const [userData, setUserData] = useState({ name: 'Alex' });
const [isSaving, setIsSaving] = useState(false);
const handleSaveClick = async () => {
setIsSaving(true); // Turn on the loading dots
try {
await api.updateProfile(userData);
alert('Saved!');
} catch (error) {
alert('Failed to save.');
} finally {
setIsSaving(false); // Turn off the loading dots
}
};
return (
);
}
Look at how much easier this is to read. We still use a state variable (isSaving), but its only job is to control the UI—disabling the button and changing the text.
The code now reads exactly how it executes: the user clicks the button, the loading state starts, the API call goes out, and the loading state stops. If the network request fails, you don’t have to check a dependency array or an if statement; you just look inside the try/catch block of the function.