Skip to content

Instantly share code, notes, and snippets.

@itsMapleLeaf
Created March 31, 2021 21:49
Show Gist options
  • Save itsMapleLeaf/2dc2c9b536016c9ca3e64f5404801c97 to your computer and use it in GitHub Desktop.
Save itsMapleLeaf/2dc2c9b536016c9ca3e64f5404801c97 to your computer and use it in GitHub Desktop.
CSS transition state helper hook
import { useCallback, useEffect, useReducer } from "react"
type TransitionState =
// the initial frame of opening, where the elements are in the DOM,
// but the transition should start in the exit state
// without this initial frame, the transition would start at the entered state
| "opening-init"
// elements are in DOM, CSS transition should start
| "opening"
// in transition finished
| "opened"
// out transition started
// this doesn't need an "init" event, because we're already at the transition in state
| "closing"
// out transition finished
// elements should not be in the DOM
| "closed"
type TransitionEvent =
| "toggle"
| "open"
| "close"
| "transitionEnded"
| "openInitFinish"
function statusReducer(
status: TransitionState,
event: TransitionEvent,
): TransitionState {
// lazy person's state machine
if (status === "closed") {
if (event === "open") return "opening-init"
if (event === "toggle") return "opening-init"
}
if (status === "opening-init") {
if (event === "close") return "closing"
if (event === "toggle") return "closing"
if (event === "transitionEnded") return "opened"
if (event === "openInitFinish") return "opening"
}
if (status === "opening") {
if (event === "toggle") return "closing"
if (event === "close") return "closing"
if (event === "transitionEnded") return "opened"
}
if (status === "opened") {
if (event === "toggle") return "closing"
if (event === "close") return "closing"
}
if (status === "closing") {
if (event === "toggle") return "opening"
if (event === "open") return "opening"
if (event === "transitionEnded") return "closed"
}
return status
}
export default function useCssTransitionState(visible: boolean) {
const [state, dispatch] = useReducer(statusReducer, "closed")
useEffect(() => {
if (visible) {
dispatch("open")
requestAnimationFrame(() => dispatch("openInitFinish"))
} else {
dispatch("close")
}
}, [visible])
return {
state,
transitionVisible: state === "opening" || state === "opened",
shouldRenderChildren: state !== "closed",
handleTransitionEnd: useCallback(() => dispatch("transitionEnded"), []),
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment