What Is useEffect Hook in React

The first time I encountered the useEffect hook, I was both intrigued and confused. Coming from a background where component lifecycle methods like componentDidMount, componentDidUpdate, and componentWillUnmount were standard in class components, useEffect felt like React’s way of saying, “Here, do all your side effects here.” It sounded powerful, but it also introduced a new way of thinking.

I quickly realized that understanding useEffect wasn’t just about knowing when it runs. It was about managing state, timing, and side effects in a controlled and predictable way. And that’s where the real challenge began.

Why effects matter

Early in my React journey, I often wondered why things like data fetching, subscriptions, and timers didn’t just go anywhere in the function body. But as I worked on more complex apps, I saw the value in isolating code that interacts with the outside world or causes re-renders.

That’s when useEffect became essential. Whether I was fetching data for a product listing or adding an event listener to track user interactions, useEffect was where I handled that logic. It became my go-to tool for running code after the component rendered and for syncing it with props or state changes.

I even started using it while working through React interview questions for freshers because many of the patterns and questions I came across revolved around proper usage of useEffect.

How it works in practice

The beauty—and the danger—of useEffect is that it runs after every render by default. That caught me off guard at first. I remember writing an effect that updated some state, thinking it would only run once. But it kept running over and over. That’s when I learned about the dependency array.

Adding [] as the second argument told React, “Only run this effect once, when the component mounts.” And adding [count] meant, “Run it only when count changes.” It felt like gaining superpowers, but it also meant I had to be very intentional about what I included in that array.

One time, I missed adding a changing value, and it led to a stale state bug that took me a while to debug. That’s when I fully appreciated how sensitive and important the dependency array is, and how quickly things can break when it’s ignored.

Handling cleanup

At some point, I had a timer running with setInterval, and I noticed it kept running even after the component was gone. That’s when I learned that useEffect supports a cleanup function. By returning a function from the effect, I could tell React what to do when the component unmounted or when the effect was about to re-run.

useEffect(() => {
  const timer = setInterval(() => {
    console.log('Tick');
  }, 1000);

  return () => clearInterval(timer);
}, []);

This pattern made it easier to manage subscriptions, timers, and even custom DOM event listeners. Without cleanup, memory leaks were almost guaranteed in many of my early projects.

Real problems I solved with useEffect

One of my most memorable experiences with useEffect was when I was building a form that autosaved input every few seconds. I used setTimeout and useEffect together, and for a while, things seemed fine. But then I realized the timeout wasn’t resetting correctly on value change.

That forced me to refactor the logic and truly understand how to control timing and dependencies within effects. The fix came down to restructuring the code and being intentional with dependencies. This challenge also helped me grasp patterns that came in handy when working on more complex UI behavior, like stateful forms and conditional rendering that I later explored in projects like React table design.

Learning from dependency mistakes

Ignoring the ESLint warnings for missing dependencies was a mistake I made more than once. I’d use a function or prop inside useEffect and skip it in the dependency array to “avoid re-renders.” But React always found a way to prove me wrong.

There was one bug where useEffect used a version of a function that didn’t include the latest state value. I couldn’t figure out why the UI wasn’t responding correctly—until I added that function to the dependency array and saw everything fall into place.

Later, while reviewing React quizzes, I noticed how often these subtle mistakes showed up in tricky questions. It made me appreciate how important it is to get these details right.

Thinking differently about side effects

Over time, useEffect trained me to separate “render logic” from “side-effect logic.” In traditional programming, you often mix everything together. But in React, rendering should be pure. It should only return JSX based on state and props—nothing else.

useEffect gives a clean boundary: this is where your component talks to the outside world. Whether it’s fetching data, updating the DOM, or logging analytics, that side of the code belongs in useEffect.

This separation made my code more predictable and easier to test. I especially noticed this when writing custom hooks. Reusing logic that relied on useEffect suddenly became clean and intuitive, and I could drop it into components without needing to refactor anything.

Final thoughts

Understanding the useEffect hook wasn’t an overnight process. It took broken components, unexpected behaviors, and several hours of debugging to realize how powerful—and sensitive—it is. But once I embraced its design, it changed how I write components completely.

Now, whenever I start a new feature, I instinctively think in terms of side effects: what needs to happen after render, and how should it respond to state or prop changes? And every time I write a new useEffect, I do it with a little more caution, and a lot more clarity.

Leave a Comment

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

Scroll to Top