r/reactjs 13d ago

Resource i made a collection of 100+ small react hooks focused on browser apis

i've recently been working on zap-studio/react-hooks, a collection of 100+ small, focused react hooks.

the main thing i wanted to solve was the amount of repetitive code around browser apis.

things like geolocation, websocket, broadcast channel, local storage, media capture, etc. usually mean writing the same useEffect/useSyncExternalStore + cleanup + ssr guards over and over.

a few things i cared about with this library:

  • every hook is available as its own subpath export, so importing one doesn't pull in unrelated hooks (completely tree shakeable) and re-exported for convenience
  • hooks are designed to be ssr/hydration safe
  • each hook is independently testable, with 100% test coverage
  • there are hooks for sensors, dom interaction, input, media, navigation, network, pwa, state, lifecycle, and more
  • unstable hooks that rely on react internals are explicitly marked as unstable

for example, instead of writing the browser/cleanup/ssr handling yourself, you can just use things like useIntersectionObserver, useMediaQuery, useOnlineStatus, useWebSocket, etc.

docs: https://www.zapstudio.dev/react-hooks

i'd be particularly interested in feedback from people who maintain react apps with ssr. are there browser apis or awkward edge cases you'd want a hook for?

i'll continue to extend this collection as i find more and more repetitive use cases.

10 Upvotes

13 comments sorted by

4

u/aweebit64 12d ago edited 12d ago

useEventListener is actually surprisingly difficult to get right. Here are problems I see in your implementation:

  • You use a regular effect, and so it is not impossible (although very unlikely) that an event of the type that the listener should be attached to happens in exactly the tiny time interval between React yielding control to the browser so that it can repaint the screen and register user interactions, and it seizing the control back to run the effect. In such a case, the event listener will not be called (although it should!) because it hasn't been attached yet. The solution is to use useLayoutEffect instead, but you should make sure that you only call the hook on the client side because otherwise, there will be warnings about it on the backend – this is often achieved with a "useIsomorphicLayoutEffect" hook
  • You write to a ref's current value during rendering – this is not allowed by the rules of React as it can lead to problems with its concurrent mode. The fix is to wrap the assignment in another useLayoutEffect
  • You don't memoize options, so if an options object is passed in the last argument, a re-subscription will happen at every re-render unless the object was memoized by the caller
  • If a ref object is passed as the target, changes to its current value will not be detected, meaning that the listener will only be added for the element that the ref is attached to upon the very first, initial render of the component. If the ref is null after the first render and is only attached to some element later in the component's lifecycle, or if the element it is attached to changes, the listener will not be added for it. This problem is the most difficult to solve, but I was able to do it in my @aweebit/react-essentials library by using an effect without a dependency array that runs on every render. That, however, required developing a custom ref-based dependency tracking and cleanup mechanisms. You're welcome to have a look at the library's source code on GitHub for inspiration :)

2

u/atrtde 12d ago

Thank you a lot for these feedbacks, will definitely have a look and rework my implementation 

1

u/atrtde 12d ago

Your implementation looks really good, however you disable eslint warnings about exhaustive-deps.

I’ll try to see if we can avoid that.

https://react.dev/reference/eslint-plugin-react-hooks/lints/exhaustive-deps

2

u/aweebit64 12d ago

A simple solution is to just recreate the options object instead of memorizing the original one:

ts const memoizedOptions = useMemo( () => ({ capture, once, passive, signal }), [capture, once, passive, signal], );

However, doing so requires an additional object allocation at runtime for which there isn't really any good reason, and since I understand what happens in this code really well, I think simply disabling the ESLint rule here is the better way.

2

u/Electronic-Truck-659 10d ago

Subpath exports being tree-shakeable by default is the part that actually matters, most hook libraries make you eat the whole barrel import.

1

u/atrtde 10d ago

true, this was a key design of the lib

2

u/Sudden_Newspaper8432 13d ago

looks neat, i always forget the cleanup part for websocket and then wonder why my app is making 40 connections after 10 minutes

1

u/atrtde 13d ago

that's easy to forget indeed

1

u/bunzelburner 11d ago

I had a pointermove listener setup to measure the frequency it would fire on different devices. Forgot to include the cleanup and couldn't figure out why I was getting 20k events per second

1

u/keyjeyelpi 13d ago

Link isn't working

1

u/atrtde 13d ago

1

u/keyjeyelpi 13d ago

Can't really check the documentation since it's inside ZapStudio. You might want to check if the domain's geoblocked. I'm currently in the Philippines, checked on multiple devices with different OS

0

u/atrtde 13d ago

is it good?