DEV Community

Alexey79
Alexey79

Posted on

A Cleaner Way to Write Conditional useEffect in React

If you've built anything in React 18+, you've likely felt the pain of Strict Mode.

You add a simple event listener, fire an analytics event, or connect a WebSocket. You boot up your dev server, andβ€”boomβ€”it fires twice.

React intentionally mounts your component, unmounts it, and remounts it in development to help you catch bugs like stale closures, missed cleanups, and memory leaks. It's an incredibly useful feature, but it can make your development console extremely noisy and trigger unexpected side-effects.

Faced with this, developers often end up doing one of three things:

  1. Disable Strict Mode entirely (and lose out on finding actual bugs).
  2. Add if (!user || !socket) return guard clauses everywhere.
  3. Stuff a useRef(false) flag inside the component to prevent double execution.

What if you could keep Strict Mode on, but express your effect timing declaratively?

Enter @okyrychenko-dev/react-effect-when.


πŸ›‘ The Problem with Manual Guards

Let's say you want to connect to a WebSocket, but you need both a userId and an authToken to be ready.

Normally, you'd write something like this:

function RealtimeConnection({ userId, authToken }) {
  useEffect(() => {
    // 1. Guard against empty state or loading moments
    if (!userId || !authToken) {
      return;
    }

    // 2. Do the actual work
    const socket = connectSocket({ userId, token: authToken });

    // 3. Clean up
    return () => socket.close();
  }, [userId, authToken]);
}
Enter fullscreen mode Exit fullscreen mode

This works, but as your app grows, this pattern repeats everywhere. The real trigger condition is hidden inside the effect body. And if you want to reduce duplicate development-time noise for a specific effect, a common workaround is adding a useRef flag alongside those conditions.

✨ The Solution: Declarative Effect Hooks

react-effect-when gives you specialized tools to lift conditions right into the hook's signature. That removes a lot of boilerplate and also provides solid TypeScript narrowing.

Let's take a look at the three main hooks the package provides.

1. useEffectWhenReady: For asynchronous data and services

When you are waiting for data, selectors, or services to load, you typically want to ensure none of them are null or undefined.

import { useEffectWhenReady } from "@okyrychenko-dev/react-effect-when";

function Profile({ user, token }) {
  useEffectWhenReady(
    ([readyUser, readyToken]) => {
      // πŸš€ TypeScript knows these are non-null and non-undefined!
      trackProfileView(readyUser.id, readyToken);
    },
    // The hook naturally tracks these dependencies:
    [user, token]
  );
}
Enter fullscreen mode Exit fullscreen mode

Why it's better:

  • No early returns. The effect only fires when all dependencies are non-null and non-undefined.
  • Built-in TypeScript narrowing. readyUser and readyToken are strictly typed, eliminating the need for user?.id checking inside the effect.

2. useEffectWhenTruthy: Perfect for boolean flags and UI states

Sometimes your conditions are simple truthy flags. For instance, you want to show a toast only when a modal actually opens, or connect a UI banner when a user is marked isOnline.

import { useEffectWhenTruthy } from "@okyrychenko-dev/react-effect-when";

function SessionBanner({ token, isOnline }) {
  useEffectWhenTruthy(
    ([readyToken, online]) => {
      connectBannerChannel(readyToken, online);
    },
    [token, isOnline],
    { once: false } // Re-run whenever it becomes truthy again!
  );
}
Enter fullscreen mode Exit fullscreen mode

Here, the effect runs when all dependencies resolve to a truthy value. If isOnline toggles off and then on again, the cleanup runs and the effect reconnects.

3. useEffectWhen: Full Control with a Custom Predicate

This is the most flexible option. It works well for cases like analytics events in development or effects that should run only when several conditions match.

import { useEffectWhen } from "@okyrychenko-dev/react-effect-when";

function ProductPage({ productId, isReady }) {
  useEffectWhen(
    ([id]) => {
      analytics.track("product_view", { productId: id });
    },
    [productId, isReady],
    ([, ready]) => ready === true, // Custom predicate
    { once: true }
  );
}
Enter fullscreen mode Exit fullscreen mode

By passing { once: true }, the effect runs once per mount lifecycle after the predicate first matches. That can reduce local Strict Mode noise for effects like analytics, while keeping the condition explicit at the call site.

You can also use custom conditions for re-synchronization. For example, syncing an item list:

useEffectWhen(
  ([itemList]) => {
    syncToServer(itemList);
  },
  [items, isOnline, hasPermission],
  // Custom logic right in the condition signature
  ([itemList, online, permission]) => 
    online === true && permission === true && itemList.length > 0 
);
Enter fullscreen mode Exit fullscreen mode

πŸ” Bonus: Debugging with onSkip

Ever wonder why an effect isn't firing? Did a socket disconnect, or did the user drop to null? The library provides an onSkip callback for logging the dependency state that failed your predicate.

import { predicates, useEffectWhen } from "@okyrychenko-dev/react-effect-when";

useEffectWhen(
  ([currentUser, currentToken]) => {
    initializeDashboard(currentUser, currentToken);
  },
  [user, token],
  predicates.ready,
  {
    onSkip: ([pendingUser, pendingToken]) => {
      console.debug("Still waiting for dependencies to load:", {
        user: pendingUser,
        token: pendingToken,
      });
    },
  }
);
Enter fullscreen mode Exit fullscreen mode

With this, you can inspect why an effect was skipped when the predicate does not match, instead of manually scattering debug logs through the component.

πŸš€ The Takeaway

We should not have to fight React's primitives. Ad-hoc useRef(false) flags and if (!data) return conditions get messy fast.

Instead, a small abstraction over useEffect can make intent clearer, reduce repeated guard boilerplate, and preserve the guardrails that React 18 gives us in development.

This is not a replacement for plain useEffect. But if your codebase has a lot of guard-heavy effects, it can be a cleaner way to express them.

If that sounds familiar, give react-effect-when a try.


Try it out:

npm install @okyrychenko-dev/react-effect-when
Enter fullscreen mode Exit fullscreen mode

If you like the approach, drop a ⭐️ on the GitHub repo and let me know what you think in the comments! πŸ‘‡

Top comments (0)