npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

use-async-effekt-hooks

v1.1.0

Published

React hooks for async effects and memoization with proper dependency tracking and linting support

Readme

use-async-effekt-hooks

React hooks for async effects and memoization with proper dependency tracking and linting support.

CI npm codecov npm downloads MIT

bundle size Types react 16-19 GitHub stars issues

Note: Tests are vibe coded. Specific tests are added when bugs are reported.

Installation

npm install use-async-effekt-hooks

Hooks

useAsyncEffekt

A hook for handling async effects with proper dependency tracking and cleanup management. The name is intentionally spelled with "k" to work correctly with react-hooks/exhaustive-deps ESLint rule.

The hook provides:

  • An isMounted callback to check if the component is still mounted
  • A waitForPrevious function to wait for previous effects and their cleanup to complete
  • Support for both synchronous and asynchronous cleanup functions

Features:

  • Proper cleanup handling - waits for async effects to complete before running cleanup
  • Race condition protection when dependencies change rapidly
  • Memory leak prevention with mount status checking
  • Sequential effect execution when needed
  • Support for both sync and async cleanup functions

Basic Usage (Without Waiting)

import { useAsyncEffekt } from "use-async-effekt-hooks";
import { useState } from "react";

function MyComponent() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);

  useAsyncEffekt(async ({ isMounted }) => {
    setLoading(true);

    try {
      const result = await fetchData();
      if (isMounted()) {
        setData(result);
        setLoading(false);
      }
    } catch (error) {
      if (isMounted()) {
        console.error("Failed to fetch data:", error);
        setLoading(false);
      }
    }
  }, []);

  if (loading) return <div>Loading...</div>;
  return <div>{data}</div>;
}

Usage with Sequential Effect Execution

When you need to ensure that previous effects complete before starting new ones:

function SearchComponent({ query }) {
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);

  useAsyncEffekt(
    async ({ isMounted, waitForPrevious }) => {
      // Wait for any previous search to complete and clean up
      await waitForPrevious();

      if (!query) return;

      setLoading(true);

      try {
        const searchResults = await searchAPI(query);
        if (isMounted()) {
          setResults(searchResults);
          setLoading(false);
        }
      } catch (error) {
        if (isMounted()) {
          console.error("Search failed:", error);
          setLoading(false);
        }
      }
    },
    [query]
  );

  return (
    <div>
      {loading && <div>Searching...</div>}
      {results.map((result) => (
        <div key={result.id}>{result.title}</div>
      ))}
    </div>
  );
}

Usage with Synchronous Cleanup

function SubscriptionComponent({ topic }) {
  const [messages, setMessages] = useState([]);

  useAsyncEffekt(
    async ({ isMounted }) => {
      const subscription = await createSubscription(topic);

      subscription.onMessage((message) => {
        if (isMounted()) {
          setMessages((prev) => [...prev, message]);
        }
      });

      // Return synchronous cleanup function
      return () => {
        subscription.unsubscribe();
        console.log("Subscription cleaned up");
      };
    },
    [topic]
  );

  return (
    <div>
      {messages.map((msg, i) => (
        <div key={i}>{msg}</div>
      ))}
    </div>
  );
}

Usage with Asynchronous Cleanup

function ConnectionComponent({ endpoint }) {
  const [status, setStatus] = useState("disconnected");

  useAsyncEffekt(
    async ({ isMounted }) => {
      const connection = await establishConnection(endpoint);

      if (isMounted()) {
        setStatus("connected");
      }

      // Return asynchronous cleanup function
      return async () => {
        if (isMounted()) {
          setStatus("disconnecting");
          await connection.gracefulShutdown();
          setStatus("disconnected");
          console.log("Connection closed gracefully");
        }
      };
    },
    [endpoint]
  );

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

Complex Usage: Sequential Effects with Async Cleanup

function ResourceManager({ resourceId }) {
  const [resource, setResource] = useState(null);
  const [status, setStatus] = useState("idle");

  useAsyncEffekt(
    async ({ isMounted, waitForPrevious }) => {
      // Ensure previous resource is fully cleaned up before acquiring new one
      await waitForPrevious();

      if (!resourceId) return;

      setStatus("acquiring");

      try {
        const newResource = await acquireResource(resourceId);

        if (isMounted()) {
          setResource(newResource);
          setStatus("ready");
        }

        // Return async cleanup to properly release the resource
        return async () => {
          setStatus("releasing");
          await newResource.release();
          setStatus("idle");
          console.log(`Resource ${resourceId} released`);
        };
      } catch (error) {
        if (isMounted()) {
          setStatus("error");
          console.error("Failed to acquire resource:", error);
        }
      }
    },
    [resourceId]
  );

  return (
    <div>
      <div>Status: {status}</div>
      {resource && <div>Resource ID: {resource.id}</div>}
    </div>
  );
}

When to Use waitForPrevious

Use waitForPrevious() when:

  • You need to ensure previous effects complete before starting new ones
  • You're managing exclusive resources (database connections, file handles, etc.)
  • You want to prevent race conditions in sequential operations
  • You need to guarantee cleanup order

Don't use waitForPrevious() when:

  • Effects can run independently and concurrently
  • You want maximum performance and don't need sequencing
  • Effects are simple and don't have interdependencies

In most cases, you should not use waitForPrevious() to keep your application responsive. It is always a trade-off between responsiveness and slower sequential execution.

useAsyncMemo

A hook for memoizing async computations with dependency tracking. Returns undefined while the async computation is in progress.

Features:

  • Automatic memoization based on dependencies
  • Preserves last successful value on error
  • Mount status checking to prevent memory leaks
import { useAsyncMemo } from "use-async-effekt";
import { useState } from "react";

function UserProfile({ userId }) {
  const userData = useAsyncMemo(
    async (isMounted) => {
      const user = await fetchUser(userId);

      // You can check if component is still mounted before expensive operations
      if (!isMounted()) return null;

      const additionalData = await fetchUserDetails(userId);

      return {
        ...user,
        ...additionalData,
      };
    },
    [userId]
  );

  // userData will be undefined while loading, then contain the result
  return (
    <div>
      {userData ? (
        <div>
          <h1>{userData.name}</h1>
          <p>{userData.email}</p>
        </div>
      ) : (
        <div>Loading user...</div>
      )}
    </div>
  );
}

ESLint Configuration

To enable dependency checking for these hooks with the react-hooks/exhaustive-deps ESLint rule, add the following configuration to your .eslintrc.js or ESLint configuration file:

module.exports = {
  // ... other ESLint configuration
  rules: {
    // ... other rules
    "react-hooks/exhaustive-deps": [
      "warn",
      {
        additionalHooks: "(useAsyncEffekt|useAsyncMemo|useAsyncMemoSuspense)",
      },
    ],
  },
};

Or if you're using .eslintrc.json:

{
  "rules": {
    "react-hooks/exhaustive-deps": [
      "warn",
      {
        "additionalHooks": "(useAsyncEffekt|useAsyncMemo|useAsyncMemoSuspense)"
      }
    ]
  }
}

This configuration tells ESLint to treat useAsyncEffekt and useAsyncMemo the same way as built-in React hooks like useEffect and useMemo, ensuring that:

  • Missing dependencies are flagged as warnings
  • Unnecessary dependencies are detected
  • Dependency arrays are properly validated

Note: The intentional spelling of useAsyncEffekt with "k" ensures it matches the regex pattern that ESLint uses to identify effect-like hooks.

API Reference

useAsyncEffekt(effect, deps?)

Parameters:

  • effect: ({ isMounted, waitForPrevious }: { isMounted: () => boolean, waitForPrevious: () => Promise<void> }) => Promise<void | (() => void | Promise<void>)> - Async function to execute. Receives an isMounted callback and a waitForPrevious function, and can optionally return a cleanup function.
  • deps?: DependencyList - Optional dependency array (same as useEffect)

Returns: void

useAsyncMemo(factory, deps?)

Parameters:

  • factory: (isMounted: () => boolean) => Promise<T> | T - Async function that returns the memoized value. Receives an isMounted callback.
  • deps?: DependencyList - Optional dependency array (same as useMemo)

Returns: T | undefined - The memoized value, or undefined while loading

useAsyncMemoSuspense(factory, deps?, options?)

Parameters:

  • factory: () => Promise<T> | T - The async function to execute.
  • deps?: DependencyList - Optional dependency array (same as useMemo).
  • options?: { scope?: string } - Optional options object.
    • scope?: string - An optional scope to isolate the cache. This is useful when you have multiple instances of the hook with the same factory and dependencies but you want to keep their caches separate.

Returns: T - The memoized value. It suspends the component while the async operation is in progress.

Important Notes:

  • SSR Environments (e.g., Next.js): In a server-side rendering environment, this hook will always return undefined on the server. The component will suspend on the client during hydration (not on initial render on the server). This means the suspense fallback will be displayed on hydration, and nothing will be displayed on the server-side render.
  • Client Component: This hook must be used within a "client component" (e.g., in Next.js, the file must have the "use client"; directive at the top).
  • Experimental: This hook is experimental and its API might change in future versions.

Example:

import { Suspense } from "react";
import { useAsyncMemoSuspense } from "use-async-effekt-hooks";

function UserProfile({ userId }) {
  const user = useAsyncMemoSuspense(async () => {
    const response = await fetch(`https://api.example.com/users/${userId}`);
    return response.json();
  }, [userId]);

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <UserProfile userId="1" />
    </Suspense>
  );
}

Features

  • ✅ Full TypeScript support
  • ✅ Proper dependency tracking
  • ✅ Compatible with react-hooks/exhaustive-deps
  • ✅ Race condition protection
  • ✅ Memory leak prevention
  • ✅ Cleanup function support
  • ✅ Error handling with value preservation
  • ✅ Lightweight and performant

License

MIT Dave Gööck