Back to Blog
ReactJune 20, 20264 min read

useState and useEffect: A Mental Model for React Beginners

Master useState and useEffect with a clear mental model. Stop guessing how React renders and start writing predictable, bug-free components today.

ReactFrontendJavaScriptHooksWeb DevelopmentNext.jsTutorial
Close-up of JavaScript code on a laptop screen, showcasing programming in progress.

I remember sitting at my desk three years ago, staring at a flickering component that wouldn't stop re-rendering. I had just discovered useEffect, and I was convinced it was the "onMount" lifecycle method I knew from older frameworks. I was wrong, and my app paid for it with roughly 1.8x more network requests than it actually needed.

If you’re struggling with these hooks, it’s usually because you’re trying to force an imperative mindset onto a declarative library. Let’s fix that.

Thinking in Snapshots with useState

When you call useState, you aren't just creating a variable. You’re asking React to keep track of a value across renders. Most beginners think const [count, setCount] = useState(0) is like a global variable that updates instantly. It isn't.

Think of each render as a frozen photograph of your component. When a render happens, the count variable is locked to the value it had when that specific function execution started.

JAVASCRIPT
const handleClick = () => {
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
};

If count starts at 0, you might expect the final value to be 3. It will actually be 1. Why? Because inside this specific render, count is 0. You’re calling setCount(0 + 1) three times. React schedules these updates, but they all resolve to the same result. When you need the previous value, use the functional update pattern: setCount(prev => prev + 1). It’s the difference between a bug and a feature.

Understanding the useEffect Mental Model

Crumpled yellow paper pieces on floor near carton box with Brain title on white background

The biggest mistake I see juniors make is treating useEffect as a "lifecycle event." We often try to map it to componentDidMount. Please, stop doing that.

useEffect is a synchronization mechanism. You are telling React: "Synchronize this piece of the DOM or this side effect with these specific values."

If your dependency array is [data], you aren't saying "run this when the component mounts." You’re saying "every time data changes, re-run this logic to keep the UI in sync." If you leave the array empty, you’re saying "this only needs to sync once."

The Wrong Turn: The Infinite Loop

I once tried to fetch user data inside a component and update state based on that data, all while listing the state variable in the dependency array. It caused an infinite loop that crashed my browser tab in about 280ms.

  1. Component renders.
  2. useEffect runs because the dependency changed.
  3. setUserData triggers a state update.
  4. State update forces a re-render.
  5. Go back to step 1.

If you find yourself in an infinite loop, check if your effect is updating the very state that triggers it. If you’re building a complex application, you might eventually move this data fetching into a dedicated layer, much like how we handle complex data structures when mastering headless WordPress with Next.js ISR. Keep your effects clean and focused on synchronization, not logic orchestration.

Why Dependency Arrays Matter

The dependency array is your contract with React. If you include a variable in the effect, you must include it in the array. If you don't, you’ll encounter "stale closures."

A stale closure happens when your effect is still looking at the variable from the first render, even though the state has updated ten times since. It’s like looking at a map from 1995 while driving through a modern city. You’ll be lost, and your users will be confused.

Practical Tips for Your Next Refactor

Close-up of AI-assisted coding with menu options for debugging and problem-solving.

  • Don't over-sync: If you can calculate a value during render, don't store it in state. You don't need useEffect to derive state from props.
  • Keep it small: If your useEffect is longer than 15 lines, you’re probably doing too much. Break it into smaller, specific effects.
  • Cleanup is not optional: If you set up a timer or a subscription, return a cleanup function. React will run this before the component unmounts or before the next effect runs.

I still find myself debugging race conditions in complex effects sometimes. It’s a part of the job. The goal isn't to write perfect code on the first try; it’s to build a mental model that lets you identify why your component is behaving strangely.

Next time you're stuck, ask yourself: "Am I trying to perform an action, or am I trying to keep the UI in sync with a value?" If you answer that honestly, the useEffect dependency array usually fills itself out. What are you still struggling with regarding these hooks? I’m still learning how to manage complex state transitions without reaching for external libraries, and there's always a cleaner way to write it.

Similar Posts