React

Stuck on useEffect? My #1 Fix for Specific Values 2025

Struggling with React's useEffect firing too often? Learn the #1 fix to run effects only when a prop or state changes to a specific value. A must-read for 2025!

D

Daniel Foster

Senior Frontend Engineer specializing in React performance and clean code patterns.

6 min read16 views

We’ve all been there. You’re working on a React component, and you need to run an effect when a certain piece of state changes. You reach for useEffect, add your state to the dependency array, and... it fires on every single change. Sometimes, that’s exactly what you want. But often, you need more precision.

What if you only want to trigger an animation when a status changes from 'pending' to 'success'? Or fetch new data only when a user ID changes from a specific old ID to a new one?

If you're wrestling with `useEffect` to make it fire only for specific value transitions, you've come to the right place. This isn't about a complex new library or a weird workaround. This is my #1, battle-tested fix that relies on core React principles. And by 2025, it's a pattern every serious React dev should have in their toolkit.

The Common Problem: useEffect is Too Eager

Let's imagine a simple OrderStatus component. It receives a status prop which can be 'pending', 'shipped', or 'delivered'. We want to show a celebratory toast notification only when the status changes to 'delivered'.

Our first attempt might look like this:

import React, { useEffect } from 'react';
import { showToast } from './toastService';

function OrderStatus({ status }) {
  useEffect(() => {
    if (status === 'delivered') {
      showToast('Your order has been delivered! 🎉');
    }
  }, [status]); // <-- The dependency

  return <div>Current Status: {status}</div>;
}

This seems logical, right? We check if the status is 'delivered' inside the effect. But there's a subtle bug here.

The Problem: The effect will run whenever status changes. If the status goes from 'pending' to 'shipped', the effect runs, the if condition is false, and nothing happens. Great. But when it goes from 'shipped' to 'delivered', the effect runs and shows the toast. Perfect!

But what if the parent component re-renders for a completely different reason, while the status prop is already 'delivered'? Because the dependency array value hasn't changed, React will skip the effect. Phew. But... what if we add another dependency? The logic starts to get complicated and brittle. The core issue is that our effect has no memory. It only knows the current state, not what it was before.

The Real Goal: Comparing the Present and the Past

To solve this, we need to answer the question: "What was the status on the previous render?" If we can get that previous value, our logic becomes trivial:

Advertisement
if (previousStatus !== 'delivered' && currentStatus === 'delivered') {
  // Now we're talking!
}

But how do we hold onto a value from a previous render? If we use useState, updating it will cause another re-render, leading to an infinite loop. This is where our hero comes in: `useRef`.

The #1 Fix: Tracking Previous Values with `useRef`

The useRef hook is perfect for this job. A ref is like a little box you can put things in. The box itself persists across renders, and changing what's inside the box (the .current property) does not trigger a re-render.

Let's apply this pattern to our component. It's a two-step process inside our useEffect: 1. Compare, then 2. Update.

Step-by-Step Implementation

Here’s how we refactor our OrderStatus component:

import React, { useEffect, useRef } from 'react';
import { showToast } from './toastService';

function OrderStatus({ status }) {
  // 1. Create a ref to store the previous status
  const prevStatusRef = useRef();

  useEffect(() => {
    // 2. Get the previous status from the ref on each render
    const prevStatus = prevStatusRef.current;

    // 3. The magic: compare previous and current status
    if (prevStatus !== 'delivered' && status === 'delivered') {
      showToast('Your order has been delivered! 🎉');
    }

    // 4. CRUCIAL: Update the ref with the current status *after* the check
    // This prepares it for the next time the effect runs.
    prevStatusRef.current = status;

  }, [status]); // We still depend on status to trigger the effect

  return <div>Current Status: {status}</div>;
}

Let’s walk through the lifecycle:

  1. Initial Render: status is 'pending'. The effect runs. prevStatus is undefined. The if is false. The ref is updated to prevStatusRef.current = 'pending'.
  2. Status -> 'shipped': The component re-renders. The effect runs. prevStatus is now 'pending'. The if condition ('pending' !== 'delivered' && 'shipped' === 'delivered') is false. The ref is updated to prevStatusRef.current = 'shipped'.
  3. Status -> 'delivered': The component re-renders. The effect runs. prevStatus is now 'shipped'. The if condition ('shipped' !== 'delivered' && 'delivered' === 'delivered') is TRUE! The toast is shown. The ref is updated to prevStatusRef.current = 'delivered'.
  4. Parent re-renders (status is still 'delivered'): React sees that status hasn't changed from the last render, so it skips running the effect entirely. Our bug is fixed!

This pattern is powerful because it's explicit and leverages the fundamental way React's lifecycle works. The effect always runs after the render, so when we read from the ref, we're always getting the value from the previous render cycle.

Level Up: The Reusable `usePrevious` Hook

The useRef logic is fantastic, but if you find yourself using it in multiple components, it’s a perfect candidate for a custom hook. This is a hallmark of a seasoned React developer: abstracting repeated logic into clean, reusable functions.

Let's create a usePrevious hook:

import { useRef, useEffect } from 'react';

function usePrevious(value) {
  const ref = useRef();
  
  // Store current value in ref after every render
  useEffect(() => {
    ref.current = value;
  }, [value]); // Rerun effect only if value changes
  
  // Return previous value (happens before the update in useEffect)
  return ref.current;
}

This tiny hook encapsulates our entire logic. It takes a value, and on every render, it returns what that value was on the previous render.

Now, look how clean our OrderStatus component becomes:

import React, { useEffect } from 'react';
import { showToast } from './toastService';
import { usePrevious } from './hooks/usePrevious'; // <-- Import our new hook

function OrderStatus({ status }) {
  const prevStatus = usePrevious(status);

  useEffect(() => {
    if (prevStatus !== 'delivered' && status === 'delivered') {
      showToast('Your order has been delivered! 🎉');
    }
    // We include prevStatus in the dependency array for correctness,
    // though the logic is driven by the change in `status`.
  }, [status, prevStatus]); 

  return <div>Current Status: {status}</div>;
}

This is so much more declarative. The component’s intent is crystal clear: "Get the previous status, and in my effect, compare it to the current status." All the implementation details are neatly tucked away in the usePrevious hook.

Conclusion: From Stuck to In Control

React hooks are powerful, but that power comes with nuance. The next time you find yourself wanting useEffect to be more specific, don't reach for complex state machines or convoluted if-ladders. Remember the core challenge: you need to know the past.

By using the useRef pattern—or even better, a clean usePrevious custom hook—you gain precise control over your effects. You're no longer just reacting to change; you're reacting to the specific story of that change.

Mastering this simple, powerful pattern is a huge step up for any React developer. It demonstrates a deep understanding of the render lifecycle and will make your components more robust, predictable, and easier to debug. Happy coding!

You May Also Like