React Custom Hooks & Animations
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.
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.
Notice ^ how we also have a Mousetrap effect that will toggle the drawer open/closed during Escape key presses.
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
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).
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.