Difference Between useState and useEffect in React

When I first began writing React components using hooks, I found myself constantly juggling between useState and useEffect. At first glance, both felt equally essential, yet I wasn’t entirely sure where one stopped and the other began. I knew they were both part of React’s modern approach to building components, but understanding their true relationship—and more importantly, their clear differences—was something that only clicked after building, breaking, and refactoring many components.

This post is a reflection on how I came to understand the distinction between these two hooks, the different roles they play in the lifecycle of a component, and how treating them correctly made my React code much more predictable.

State controls data

Whenever I need to track some kind of data inside a component—whether it’s user input, a toggled modal, or a loading indicator—I turn to useState. It’s my way of declaring: “Here’s a piece of data that should trigger a re-render whenever it changes.”

In one of my early projects, I was building a product filter UI. Each time a user checked a box or typed into a search field, I needed the UI to update in real time. Using useState allowed me to store their input and reflect it immediately in the rendered product list.

But I learned quickly that useState isn’t just about declaring data—it’s about controlling when and how the component updates. Every time I call the setter function from useState, React schedules a re-render. That re-render re-evaluates the JSX but does not re-run effects unless the data changes are part of the effect’s dependency array. That detail matters a lot more once you start mixing useState and useEffect together.

effects handle side actions

While useState deals with data, useEffect deals with consequences of that data. It’s where I put logic that should run because something changed, not just because I’m rendering new UI.

I remember adding a search input to one app and setting the search value using useState. But I didn’t want to fire an API call on every single keystroke. That’s where useEffect came in—I watched the searchTerm state, and when it changed (with some debounce logic), I used useEffect to trigger the API request.

This was a huge lesson: useEffect doesn’t hold data. It reacts to data. I’ve come to think of useState as a storage box and useEffect as the light switch that turns on when the box’s contents change.

There were times I made the mistake of putting everything in useEffect, even state updates. That only led to unnecessary complexity and weird behavior—especially when working with APIs or intervals. Eventually, I embraced the idea that useState should remain focused on internal data, and useEffect should handle communication with the outside world: APIs, local storage, browser events, etc.

One without the other

Some components I’ve written need only useState. A button toggle that shows or hides some UI? No need for useEffect there. But whenever I need to react to a state change—say, to log something, update the document title, or fetch data—useEffect comes into play.

I ran into this while working on more advanced interactions, like autosaving input to a server or updating the URL query params. That’s when I had to use both hooks together—useState to track the value and useEffect to respond to its changes. It’s a pattern I’ve repeated often, especially in real-world components like forms, filters, and loaders.

This combo showed its true power when building components that needed real-time syncing or conditional logic. I’d use useState to track the raw value and then shape how and when to act on it using useEffect. These were patterns I saw reinforced while reviewing topics like React testing library queries or working through component behaviors in various interview prep exercises.

They live in different layers

One thing that helped me mentally separate these hooks was thinking in terms of layers. useState lives in the UI layer—it’s concerned with what the component renders and how it responds visually. useEffect, on the other hand, lives in the behavioral layer—it’s concerned with what the component does after it renders.

When I kept those roles clear in my mind, I avoided writing effects that tried to mimic or replicate state behavior. For instance, I once used useEffect to store fetched data into a separate state unnecessarily, leading to double updates and extra renders. Simplifying it to let the useEffect fetch the data and immediately call setData made things cleaner and faster.

Mistakes that taught me

A mistake I made often in the beginning was using useEffect to update state and then watching that same state in the dependency array. This sometimes caused unnecessary loops. In some rare cases, it even led to infinite loops when I didn’t conditionally handle updates properly.

This was particularly painful when I built a timer feature. I tried to track elapsed time using useState, and the useEffect kept resetting the timer. It wasn’t until I broke the logic into smaller parts—one for state and one for effects—that it finally clicked.

I’ve since learned to isolate what truly needs to go inside an effect and what can remain inside the component body or within handlers. And whenever possible, I make use of patterns like defining functions inside the effect itself or using useCallback when stability is critical.

Final thoughts

Understanding the difference between useState and useEffect wasn’t just about learning what they do—it was about respecting their boundaries. One holds data; the other responds to data. One drives the UI; the other watches the system.

Every time I start writing a new feature, I now ask myself two questions: “What does my component need to remember?” and “What does my component need to do when that memory changes?” The answers point me to useState and useEffect—each with its own role, each essential in its own right.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top