Composing Behavior with React Hooks
When React was first released, we were taught to break down our screens into composable UI components. Now with React Hooks, we should take the next step of breaking down our components into composable behaviors.
Scenario: Notification Banner
Let’s talk through a contrived, but semi-real world scenario. We want to display notifications on a banner bar at the stop of our app.
- Users should be able to click a button to go to the previous or next notification.
- The notifications should automatically page every 5 seconds
- If a user is hovered over the banner, the auto-paging should pause.
- The notifications from an API endpoint.
If you’re interested, I’ve implemented the Notification Banner in the “old” way, a Class component. It’s not bad. But it’s starting to get a little big, and not very reusable.
Now with React Hooks, we should take the next step of breaking down our components into composable behaviors.
Looking at the behavior of NotificationBanner
I see….
Manual Paging — Keeps track of the current page and provides methods to update the current page.
Auto Paging — Calls the Manual Paging’s goForward method every N milliseconds, unless the user is hovered. That seems a little complicated still. Lets pull out more behavior
Set Interval —registering something to happen every N milliseconds and safely unregistering when the component goes away
Hovering — tells you whether a user is currently hovered over a target element.
If we were to encapsulate each of these 4 behaviors into individual React Hooks, we being to see the power of this pattern. The behaviors are now individually consumable by OTHER React components.
- Do you have another component that displays a big tooltip on hover? That’d be easy with a
useHover
hook. - Do you have a Search Results view that implements paged results? How about refactoring to a
usePaging
hook. - Have a screen that should check the API for updated data every 30 seconds? We could create
useInterval
hook to handle all registering and unregistering the interval.
Also, the behaviors are individually composable by other more complex custom hooks.
usePaging
The usePaging
hook just needs to be given the total number of pages. It will be used like this:
let { currentPage, goBack, goForward } = usePaging(items.length)
It’s primary job is to utilize the builtin useState
hook to keep track of the current page. It provides methods to update the currentPage
so that you don’t have to deal with the logic of moving outside the bounds of your item set.
useInterval
The useInterval
hook will be used like this:
useInterval(goForward, 5000);
It handles registering the setInterval
as well as cleaning itself up by calling clearInterval
. If you pass a delay of 0
, nothing happens; effectively clearing/pausing the interval.
Some advanced details are that it supports swapping out the callback function or modifying the delay at any time.
All credit goes to Dan Abramov for this one: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
useHover
The useHover
hook gives you a ref
you can use to target any element you want. It also gives you the isHovered
status. This value will be set to true
as soon as the mouse enters the ref’d element.
let [hoverRef, isHovered] = useHover();
You can add the hoverRef
to any dom element with ref={hoverRef}
.
useAutoPaging
The last hook, useAutoPaging
composes the previous 3 hooks to provide some advanced behavior.
- It takes in the total number of pages and give back
currentPage
as well as methods to updatecurrentPage
via theusePaging
hook - It automatically increments the page every 5 seconds via the
useInterval
hook - It pauses the interval based on the
isHovered
value fromuseHover
let { currentPage, pauseRef, goForward, goBack } = useAutoPaging(items.length);
That’s it, our NotificationBanner
component is a lot simpler now and we have all of this reusable logic we can sprinkle across our projects.
Check out the full code example here: