React Custom Hooks & Animations
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 the
onChange callback to support notifications to external listeners regarding state changes.
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
Drawerfunctional 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.
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 function
makeTimeline() 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 specified
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!
Here is the codesandbox.io demo (with jest tests).
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 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.
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.