React Custom Hooks & Animations

Thomas Burleson
5 min readJan 5, 2020

--

Using state machines is magical when combined with React custom hooks. Recently Dave Geddski wrote about why state machines are fantastic and how to use them in: State Machines in React.

Using the amazing XState library on Github, Dave showed how state machines can force UI components to (a) maintain valid states and (b) implement transitions from one (valid) state to another.

Consider the XState visualizer diagram below. This diagram shows only four (4) states are possible for the Drawer component: closed, opening, open, and closing.

I especially liked the part of that demo that showed how to use GSAP Animations to asynchronously animate the Slide-in Menu Drawer UI during the transitions through the opening/closing states.

When the state is “opening” then the openMenu animation is called. Best of all, when the animation finishes the state is auto-changed from “opening” to “open.”

Another cool feature [implemented in the Drawer] was the use of theonChange callback to support notifications to external listeners regarding state changes.

Demo: Slide-In Menu Drawer

Custom Hooks

While playing with the demo and leveling-up on XState, I realized that the demo could be improved using custom hooks.

Dave was already using React hooks with the Drawer functional component. So why would I want to implement a custom hook?

Dave’s solution had too much logic in the view component. The result I wanted was a refactored, elementary JSX view component and a separate, custom hook that:

  • hide the complexity of animations
  • properly kills any in-progress animations
  • expose a simple API for use in view components

Investigating what data the view actually uses, it only needs the nextState value and a function toggleDrawer to open and close the slide Drawer. This means that the custom hook API would be simply:

const [nextState, toggleDrawer] = useDrawerHook(elRef, onChangeFn);

Using a Custom hook, I was able to radically reduced the JSX code size and complexity… now the Drawer.jsx functional component is super-easy to maintain and test.

Drawer.jsx

Notice ^ how we also have a Mousetrap effect that will toggle the drawer open/closed during Escape key presses.

Refactoring using a custom hook

Animations

Since the state could change while the drawer is still animating the opening or closing, subsequent state changes must also kill any ‘in-progress’ animations.

With a timeline factory functionmakeTimeline() we can use the new APIs in GSAP v3 to easily kill() an in-progress animation.

Notice — if animations are disabled — we immediately resolve the promise which tells the XState machine to go to onDone state. E.g. opening open

DrawerHook.js

Whenever the state change (and animation) is done, XState automatically updates the current state object… and our internal useEffect will notify external observers using the specifiedonChange callback.

Jest Testing

By separating the custom hook code to an external module (@see DrawerHook.js), both the JSX and hooks can be easily tested.

Now I can directly test the DrawerHook with animations enabled.

Animations presented some exciting challenges to testing the JSX component with the @testing-library/react. When testing the Drawer.jsx, I was forced to pass in an optional flag disableAnimation. This option enabled me to bypass animations when testing the JSX component itself.

The CodeSandbox.io Demo has Jest tests implemented!

Demo

Here is the codesandbox.io demo (with jest tests).

Codesandbox.io Demo: Sliding Drawer

State Machines

State Machines are important when you want to enforce a specific set of states for a target object. XState supports async actions and services that can be used as part of state transitions.

Rather XState is used to ensure and guide valid state changes within a single context… in our case that is the Drawer view component.

And when async animations are involved as part of the state changes — like the opening or closing states used in the Drawer component — then XState can be very very beneficial.

State machines do not address the need to manage and share state between multiple components. State machines are not intended for generalized state management.

State Management

State management systems ensure application 1-way data flows, immutable data, centralized change management, and sharing of structures that include collections of entities, etc.

State management is hard!

Here are important questions to consider when thinking about multi-component state management:

  • Who keeps track of state?
  • How can state get changed?
  • What state goes where?
  • How do we effectively debug?
  • How do we get notified of specific state changes?
  • How do we optimize state change notifications?

To know when to use State Machines or State management developers should review the acronym ‘SHARI’:

If your state is not shared, then use state machine patterns. If sharing with multiple components is important, use state management patterns.

When developers require state management, libraries like Akita should be used.

Summary

Custom hooks are an incredibly useful and efficient way to partition and separate logic from view components. Custom hooks enable developers to prepare specific APIs.

Sometimes those APIs are generic and useful as a library set. For example useObservable(...) is a custom hook that manages subscriptions to an observable stream.

Sometimes APIs are specific to the custom view components. For example, useDrawerHook(...) is a custom hook intended only for the Drawer view component.

This custom hook is nevertheless important for testing and maintenance.

--

--

Thomas Burleson

Solutions architect expert in NextJS, React and Angular solutions. Formerly a Principal Architect @ Nrwl.io, Team Lead for Angular, Google.