Composing Behavior with React Hooks

Andrew Petersen
3 min readFeb 19, 2019

--

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 update currentPage via the usePaging hook
  • It automatically increments the page every 5 seconds via the useInterval hook
  • It pauses the interval based on the isHovered value from useHover
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:

--

--