Compare
How react-tils compares
The same 10 everyday tasks in seven React hook libraries, rendered on a real server, hydrated in a browser, checked for leaked listeners, and measured. Everything on this page is generated by a script you can run yourself.
- react-tils passes every check on all 10 of 10 tasks. 5 of the 6 other libraries throw on the server or cause a hydration mismatch on at least one, with default settings.
- Smallest hook on 4 of 10 tasks. @uidotdev/usehooks is often a few bytes smaller, but its hooks for browser state are client-only: they throw during server rendering.
- Only the hooks you import ship to your users. The whole-library sizes are listed at the end for reference.
Server rendering
Each hook is rendered with renderToString in plain Node, then the HTML is hydrated in a desktop-sized browser environment, then mounted and unmounted under StrictMode to find listeners left behind. Hover a mark for the error.
| Task | react-tils | @uidotdev/usehooks | usehooks-ts | @mantine/hooks | react-use | @reactuses/core | ahooks |
|---|---|---|---|---|---|---|---|
| Media query | ✓useMediaQuery | ✕useMediaQuery | !useMediaQuery | ✓useMediaQuery | !useMedia | !useMediaQuery | — |
| Window size | ✓useWindowSize | ✓useWindowSize | !useWindowSize | ✓useViewportSize | !useWindowSize | ✓useWindowSize | — |
| Window scroll | ✓useWindowScroll | ✓useWindowScroll | — | ✓useWindowScroll | ✓useWindowScroll | ✓useWindowScroll | ✓useScroll |
| Element in view | ✓useInView | ✓useIntersectionObserver | ✓useIntersectionObserver | ✓useInViewport | ✓useIntersection | ✓useIntersectionObserver | ✓useInViewport |
| localStorage state | ✓useLocalStorage | ✕useLocalStorage | ✓useLocalStorage | ✓useLocalStorage | ✓useLocalStorage | ✓useLocalStorage | ✓useLocalStorageState |
| Click outside | ✓useClickOutside | ✓useClickAway | ✓useOnClickOutside | ✓useClickOutside | ✓useClickAway | ✓useClickOutside | ✓useClickAway |
| Keyboard shortcut | ✓useHotkey | — | — | ✓useHotkeys | ✓useKey | — | ✓useKeyPress |
| Debounced value | ✓useDebouncedValue | ✓useDebounce | ✓useDebounceValue | ✓useDebouncedValue | ✓useDebounce | ✓useDebounce | ✓useDebounce |
| Debounced callback | ✓useDebouncedCallback | — | ✓useDebounceCallback | ✓useDebouncedCallback | — | ✓useDebounceFn | ✓useDebounceFn |
| Online status | ✓useOnline | ✕useNetworkState | — | ✓useNetwork | !useNetworkState | !useOnline | !useNetwork |
| Passes every check | 10 / 10 | 5 / 8 | 5 / 7 | 10 / 10 | 6 / 9 | 7 / 9 | 7 / 8 |
- ✓ Renders on the server, hydrates without a mismatch, and leaves no listeners behind.
- ! Hydration mismatch: the browser's first render differs from the server HTML, so React throws the server HTML away and re-renders.
- ✕ Throws during server rendering, so the page fails unless you avoid rendering it on the server.
- — No equivalent hook.
With each hook's default settings, as most code calls it. Some mismatches can be avoided with an option: initializeWithValue: false in usehooks-ts, or a default state in react-use's useMedia and @reactuses/core's useMediaQuery.
Size per hook
Bytes after minifying and compressing with brotli, React excluded: what a bundler adds to your app for that one hook. The smallest in each row is highlighted.
| Task | react-tils | @uidotdev/usehooks | usehooks-ts | @mantine/hooks | react-use | @reactuses/core | ahooks |
|---|---|---|---|---|---|---|---|
| Media query | 150 B | 181 B | 968 B | 256 B | 208 B | 318 B | — |
| Window size | 194 B | 159 B | 1373 B | 213 B | 414 B | 861 B | — |
| Window scroll | 191 B | 286 B | — | 278 B | 388 B | 4451 B | 622 B |
| Element in view | 268 B | 234 B | 1184 B | 209 B | 201 B | 4208 B | 702 B |
| localStorage state | 389 B | 362 B | 1392 B | 791 B | 394 B | 5704 B | 1101 B |
| Click outside | 214 B | 184 B | 994 B | 294 B | 269 B | 4325 B | 576 B |
| Keyboard shortcut | 611 B | — | — | 710 B | 382 B* | — | 2167 B |
| Debounced value | 128 B | 120 B | 1252 B | 284 B | 252 B* | 1249 B | 1484 B |
| Debounced callback | 309 B | — | 1167 B | 478 B | — | 1200 B | 1446 B |
| Online status | 134 B | 377 B | — | 365 B | 375 B | 425 B | 410 B |
| Whole library | 12.0 kB | 4.1 kB | 4.7 kB | 21.3 kB | 20.3 kB | 23.9 kB | 22.9 kB |
- * Keyboard shortcut, react-use: One key; no modifiers like mod+k, no sequences.
- * Debounced value, react-use: A debounced effect: you set state in it yourself.
- Whole-library sizes cover different numbers of hooks; with tree shaking, only the hooks you import count.
How this was measured
- Versions: react-tils 0.1.5, @uidotdev/usehooks 2.4.1, usehooks-ts 3.1.1, @mantine/hooks 9.7.1, react-use 17.6.1, @reactuses/core 6.5.9, ahooks 3.10.0. Measured on 2026-10-12.
- Server:
renderToStringin a Node environment with no DOM at all, as on a real server. - Hydration: the server HTML is hydrated with
hydrateRootin happy-dom with a 1024×768 window, online, and an empty localStorage. A mismatch is any recoverable hydration error React reports. - Leaks: the hook is mounted and unmounted under StrictMode; any event listener added and not removed fails it. These are the same checks react-tils runs on its own hooks.
- Size: each hook imported on its own and bundled with size-limit, React excluded, so every library is measured the same way.
- Not measured here: whether a hook shows the right value on its first frame after client-side navigation, and React Compiler support.
The script and every hook call are in compare/. Run it with pnpm compare. Spotted an unfair call? Open an issue.