Stop Overusing useState in React.
The Syncing Trap: How We End Up in useEffect Hell
The biggest culprit behind state overconsumption is the desire to sync data.
Let’s look at a realistic scenario: you are building a simple product list dashboard. You want to track the array of items, but you also need to display the total number of items and the total price at the bottom of the screen.
As a beginner, you might write code that looks exactly like this:
const [items, setItems] = useState([]);
const [totalItems, setTotalItems] = useState(0);
const [totalPrice, setTotalPrice] = useState(0);// Syncing the extra states every time items change
useEffect(() => {
setTotalItems(items.length);
const total = items.reduce((sum, item) => sum + item.price, 0);
setTotalPrice(total);
}, [items]);
On the surface, this feels incredibly logical. You think: “When the items change, I need to recalculate the totals and save them into state so the UI updates.”
But behind the scenes, you have accidentally introduced a major performance flaw.
The Double-Render Punishment
Every time you call a state update function, React is forced to re-render the screen. Let’s look at the actual execution timeline of the code above when a user adds a new item to their cart:
- The Trigger: The user adds a product.
setItemsis called with the new array. - Render #1: React runs the component to update the list of items on the UI.
- The Effect Fires: After the screen is drawn, React notices
itemschanged and triggers youruseEffect. - The Second State Trigger: Inside the effect, you call
setTotalItemsandsetTotalPrice. - Render #2: React is forced to stop what it’s doing and re-render the entire component a second time just to update those numbers.
You are forcing React to do double the work for a single user action. In a large scale app, this pattern leads to lagging interfaces and out-of-sync bugs that are a total headache to debug.
The Answer: Derived State
The golden rule of React state management is simple: If a value can be calculated from existing props or state, it should not be kept in state.
Instead of storing it, you calculate it on the go during the render phase. This is called Derived State, and it requires absolutely no hooks, no effects, and zero state synchronization.
Let’s refactor that exact same shopping cart component using regular JavaScript:
const [items, setItems] = useState([]);
// No hooks, no effects. Just plain JavaScript.
const totalItems = items.length;
const totalPrice = items.reduce((sum, item) => sum + item.price, 0);
Look at how much cleaner that is. We cut our code down significantly, but more importantly, let’s look at how React processes this now:
- The Trigger: The user adds a product.
setItemsis called. - Render #1: React re-runs the component function from top to bottom.
- Instant Calculation: As the function executes, JavaScript instantly reads the fresh
itemsarray, counts the length, sums up the price, and injects the updated variables straight into the JSX layout.
One user action. One clean render cycle. Perfect synchronization, every single time.
The Lesson Learned
When you are starting out, deleting code feels terrifying. We treat hooks like a safety blanket — if we don’t explicitly put data into a React hook, we worry React will somehow forget about it.
The major landmark in moving from a beginner to an intermediate developer is realizing that React components are just functions that run repeatedly. Clean out those extra hooks, delete those unnecessary
useEffects, and watch your code instantly become leaner and legible.