Minimal React Transition Hook (useTransition
)
I often find myself in the need of a very basic open/close transition in React for components like dialogs, side sheets, or dropdowns. The goto solution for a while seemed to be React Transition Group
, which I never understood how to use properly. An excellent solution for animations is react-spring
, but I'd consider it an overkill for a basic CSS powered open/close transition (but it's great for animations in something like an image viewer).
This is why I’ve ended up writing my own hook: react-css-transition-hook
It is used like this:
;
;
if !isVisible
return
<div>
...
</div>
Isn’t that easy to understand and reason about just from reading the usage? Here is a complete example using the hook: Demo, Source.
The hook itself is just ~50 lines long (excluding the typings and doc comments) and so simple, it easily fits into this post:
This is exactly what I wanted. Simple, small, no fancy magic - just using basic useState
, useEffect
, and useCallback
hooks.
Let’s dissect its inner workings top-down.
Typically, when a component is closed, it is just not rendered anymore. This does not work well with a close transition, because it is necessary to keep the component in the DOM until the close transition finished. This is why the hook takes the desired state (visible or not; isOpen
in the usage example above, and desiredState
in the code above) as an input, and returns whether you should still render the component or not (isVisible
in the example usage above, and currentState
in the code below).
;
;
When the hook is first used, it determines what the initial state is and also provides an option to skip the enter transition if it starts being visible right away. It also sets its initial transition state (transition
), which is either entered
, if the component is already visible, or null
if it is not.
,;
When either the current or desired states change, it updates the active transition accordingly:
- Not visible right now (
currentState === false
), but should be shown (desiredState === true
): Render the component and setentering
(usually something like 0% opacity, or moved outside of the screen) as the active transition. - Visible right now (
currentState === true
), but should not be shown anymore (desiredState === false
): Set active transition toexiting
(often the same asentering
, so something like 0% opacity, …) and keep the component for now.
For the open transition, the transition cannot be set to entered
right away. It is always necessary to render the component with entering
first so that there is a starting point for the transition to be based on. Example:
- Render with
0%
opacity, and once that is reflected in the DOM, - Set the opacity to
100%
for the transition to start.
This is what the second useEffect
is for.
,;
The second useEffect
cannot be integrated into the first one, because there needs to be a DOM update before the state changes of the second useEffect
are applied. By separating them, the state changes from the first effect are reflected in the DOM before the whole hook is called again and applies the changes from the second effect. The second effect is thereby simply reacting on the changes from the first useEffect
and kicks of the transition by moving from entering
to entered
, or from exiting
to exited
.
;
It is necessary to know when the close transition finished so that the component can be removed from the DOM. This is achieved by a simple onTransitionEnd
event handler. Once fired, it sets the current state to false
and resets the transition to null
.
That’s all there is to it.
Finally, as a small bonus, an advanced example of how to use it for a Radix UI Dialog-based side sheet:
;
;
;
;
;
Comments: dev.to