DEV Community

Senthil Kumar
Senthil Kumar

Posted on

useThisHook 1.3.0: 13 new React hooks for async work, offline queues, and debugging re-renders

I keep writing the same handful of hooks in every React project I touch. There's the "loading / error / data" state machine, the retry loop with backoff, and the beforeinstallprompt listener I have to look up every single time. useThisHook is where I put those hooks so I can stop rewriting them.

If you haven't seen it before: usethishook is a collection of typed React hooks plus promise-based confirm() / prompt() dialogs. It has zero runtime dependencies, it's SSR-safe, it's tree-shakeable, and it supports React 18+. You import only what you use.

Version 1.3.0 adds 13 new hooks, which brings the total to 53. It's a minor release with no breaking changes.

npm i usethishook@^1.3.0
Enter fullscreen mode Exit fullscreen mode

Here's what's new, with the kind of code I actually write with these hooks.

Async hooks

useAsyncState: loading/error/data without the boilerplate

You pass it a function that receives an AbortSignal. It tracks status ('idle' | 'loading' | 'success' | 'error') for you, and it aborts the previous call when you start a new one.

import { useAsyncState } from 'usethishook';

export const UserCard = ({ userId }: { userId: string }) => {
  const { data, error, isLoading, execute } = useAsyncState(
    (signal) => fetch(`/api/users/${userId}`, { signal }).then((r) => r.json()),
    { immediate: true, source: userId }, // refetch when userId changes
  );

  if (isLoading) return <p>Loading…</p>;
  if (error) return <button onClick={() => execute()}>Retry</button>;
  return <p>{data?.name}</p>;
};
Enter fullscreen mode Exit fullscreen mode

It also returns abort, reset, and the isIdle / isSuccess / isError flags. A response from an aborted call never overwrites newer state.

useOptimisticAction: update now, roll back if the server says no

You give it an initial value, the real async action, and a function that computes the optimistic value. If the action fails, the value goes back to what it was before. While a call is in flight, further calls are ignored, which guards against double-clicks.

import { useOptimisticAction } from 'usethishook';

const { value: liked, run, isPending, error } = useOptimisticAction(
  false,
  async (next: boolean) => {
    await api.setLike(postId, next);
    return next; // the confirmed value
  },
  (current) => !current, // what the UI shows immediately
);

<button onClick={() => run()} disabled={isPending}>{liked ? 'Unlike' : 'Like'}</button>;
Enter fullscreen mode Exit fullscreen mode

useRetry: retries with backoff and a real cancel

It wraps any async function. By default it makes 3 retries with exponential backoff, starting at 250 ms with a factor of 2 and capped at 30 s. All of that is configurable, and retryIf lets you skip retries for errors that won't fix themselves.

import { useRetry } from 'usethishook';

const { run, cancel, attempt, isPending, error } = useRetry(
  (orderId: string) => api.submitOrder(orderId),
  {
    retries: 4,
    backoff: 'exponential',
    delayMs: 500,
    retryIf: (err) => !(err instanceof ValidationError),
  },
);
Enter fullscreen mode Exit fullscreen mode

cancel() interrupts a pending backoff wait, and attempt tells you which try you're on. That's handy for a "Retrying (2/5)…" label.

useAsyncQueue: run tasks one at a time (or N at a time)

This is a FIFO queue with a concurrency cap. enqueue returns a promise for that task's result.

import { useAsyncQueue } from 'usethishook';

const { enqueue, size, pending, clear } = useAsyncQueue({ concurrency: 2 });

const uploadAll = (files: File[]) =>
  Promise.allSettled(files.map((file) => enqueue(() => uploadFile(file))));
Enter fullscreen mode Exit fullscreen mode

size is how many tasks are waiting and pending is how many are running. clear() rejects the tasks that haven't started yet.

usePolling: polling that respects hidden tabs

By default it pauses while the tab is hidden and runs once immediately. Pass null as the interval to stop it. Each tick gets an AbortSignal, and a slow request is aborted when the next tick starts.

import { usePolling } from 'usethishook';

const { isPolling, error } = usePolling(
  async (signal) => setJob(await fetchJob(jobId, signal)),
  job?.done ? null : 5_000,
);
Enter fullscreen mode Exit fullscreen mode

Browser and PWA hooks

useOnlineQueue: a small in-memory outbox

You give it a function that processes one item. When the browser is online, items flush in order. When it's offline, they wait.

import { useOnlineQueue } from 'usethishook';

const { enqueue, items, isOnline, isFlushing } = useOnlineQueue(
  (note: Note) => api.saveNote(note),
);
Enter fullscreen mode Exit fullscreen mode

Be aware that the queue lives in memory, so a page reload loses it. If an item fails, flushing stops at that item and the hook exposes the error. It doesn't replace a service-worker sync strategy, but it covers the common "user clicked save on a flaky connection" case.

useCrossTabState: state that follows you across tabs

It has the same tuple shape as useState. The value is stored as JSON in localStorage and synced to other tabs through BroadcastChannel, with the storage event as a fallback.

import { useCrossTabState } from 'usethishook';

const [theme, setTheme] = useCrossTabState<'light' | 'dark'>('app-theme', 'light');
Enter fullscreen mode Exit fullscreen mode

useInstallPrompt and useStorageQuota

useInstallPrompt captures the beforeinstallprompt event so you can show your own "Install app" button.

const { isReady, isInstalled, prompt } = useInstallPrompt();

{isReady && !isInstalled && (
  <button onClick={async () => console.log(await prompt())}>Install app</button>
)}
Enter fullscreen mode Exit fullscreen mode

prompt() resolves to 'accepted', 'dismissed', or 'unavailable'.

useStorageQuota wraps navigator.storage.estimate() and returns usage, quota, usageRatio, isSupported, and a refresh() function. It's useful before you cache something large offline.

Interaction hooks

useKeyboardShortcut

It listens for key combos on window and cleans up after itself. mod maps to Cmd on Apple platforms and Ctrl everywhere else. By default it ignores keystrokes inside inputs, textareas, selects, and contenteditable elements, and it calls preventDefault.

import { useKeyboardShortcut } from 'usethishook';

useKeyboardShortcut('mod+k', () => setPaletteOpen(true));
useKeyboardShortcut('escape', () => setPaletteOpen(false), { enabled: paletteOpen });
Enter fullscreen mode Exit fullscreen mode

useLongPress

It returns mouse and touch handlers that you spread onto an element. The defaults are a 500 ms delay and a 10 px move threshold. If the pointer moves past the threshold, the press is cancelled, so scrolling on mobile doesn't trigger it.

const pressHandlers = useLongPress(() => openContextMenu(item), { delayMs: 600 });

<li {...pressHandlers}>{item.title}</li>;
Enter fullscreen mode Exit fullscreen mode

Debug hooks (quiet in production)

These are two small hooks I reach for when a component re-renders more than it should. Both log through console.debug and are disabled by default when NODE_ENV is production. You can override that with { enabled }.

import { useRenderTracker, useWhyDidYouUpdate } from 'usethishook';

const renders = useRenderTracker('ProductList');
useWhyDidYouUpdate('ProductList', { filters, sort, onSelect });
// [useWhyDidYouUpdate] ProductList { onSelect: { from: ƒ, to: ƒ } }
Enter fullscreen mode Exit fullscreen mode

When you see that onSelect line, you know an inline callback is the problem and not your data.

Beyond the hooks

  • MCP catalog: usethishook-mcp, a separate package, now covers all 53 hooks through list_hooks, search_hooks, get_hook, and recommend_hook. That means Cursor or Claude Desktop can suggest the right hook instead of writing a new one. Setup is on the MCP page.
  • Playground: there's a new comparison page vs usehooks-ts / react-use, and every hook page now has StackBlitz and CodeSandbox "try online" links.

Upgrading

There are no breaking changes and no changes to existing hook APIs in this release. 1.3.0 only adds new exports, so bumping the version is enough:

npm i usethishook@^1.3.0
Enter fullscreen mode Exit fullscreen mode

Try it and tell me what's missing

If one of these hooks saves you an afternoon, a star on GitHub helps more people find it. I'd love to hear which hook you keep rewriting in your own projects, too. Open an issue or a PR, or drop a comment below. That's how most of this release came together.

Top comments (0)