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
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>;
};
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>;
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),
},
);
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))));
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,
);
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),
);
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');
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>
)}
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 });
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>;
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: ƒ } }
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 throughlist_hooks,search_hooks,get_hook, andrecommend_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
Try it and tell me what's missing
- npm: https://www.npmjs.com/package/usethishook
- GitHub: https://github.com/senthilkumar979/useThisHook
- Release notes: https://github.com/senthilkumar979/useThisHook/releases/tag/1.3.0
- Playground: https://usethishook.mentorbridge.in/
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)