Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
import { useState, useEffect, useRef } from 'react';
import useIsMounted from './use-is-mounted';
const RENDER_TIMEOUT = 35;
export default function useMountTransition({
shouldBeMounted,
transitionDurationMs,
onEnteringTimeout = RENDER_TIMEOUT,
}) {
// We track all of our state on one object. This is to prevent strange issues
// that can occur when they update as independent state values.
// I believe that the strange issues occur due to this component's usage of
// timeouts.
const defaultState = {
shouldBeMounted,
shouldRender: shouldBeMounted,
useActiveClass: shouldBeMounted,
};
const [transitionState, updateTransitionState] = useState(defaultState);
// This is just a li'l utility to make it easier to update our state.
// This is here because `useState` doesn't merge the new with the old.
function makeChanges(newState) {
return prevState => {
return {
...prevState,
...newState,
};
};
}
// We need to keep track of whether we have been rendered or not. Otherwise,
// this this will try and unmount something that was never mounted the first time
// the hook is called.
const isMounted = useIsMounted();
// This is the time when the latest enter transition began. We use it to
// determine how much time would be necessary to transition out if the enter
// transition is interrupted by a `shouldBeMounted: false`
const startTimeMs = useRef();
// Timeout references
const onCallEnterTimeoutRef = useRef();
const onEnterTimeoutRef = useRef();
const onExitTimeoutRef = useRef();
useEffect(() => {
// When this component unmounts, we clear out all of the timers
return () => {
clearTimeout(onCallEnterTimeoutRef.current);
clearTimeout(onEnterTimeoutRef.current);
clearTimeout(onExitTimeoutRef.current);
};
}, []);
// Subscribe to changes in the passed-in `shouldBeMounted` value.
// Again, you may be wondering: why not use it directly? The reason is that I was running
// into (hard-to-replicate) bugs where the state reconciliation was weird in the setTimeout
// callbacks. Keeping all relevant state on a single object ensures that things don't
// get in a whacky state.
useEffect(() => {
updateTransitionState(
makeChanges({
shouldBeMounted,
})
);
}, [shouldBeMounted]);
//
useEffect(
() => {
if (isMounted && !transitionState.shouldBeMounted) {
clearTimeout(onEnterTimeoutRef.current);
clearTimeout(onCallEnterTimeoutRef.current);
let closeDuration = transitionDurationMs;
if (startTimeMs.current !== null) {
const endTimeMs = new Date().getTime();
const elapsedTime = endTimeMs - startTimeMs.current;
closeDuration = elapsedTime;
}
updateTransitionState(
makeChanges({
useActiveClass: false,
})
);
onExitTimeoutRef.current = setTimeout(() => {
updateTransitionState(
makeChanges({
shouldRender: false,
})
);
}, closeDuration);
} else if (transitionState.shouldBeMounted) {
clearTimeout(onExitTimeoutRef.current);
updateTransitionState(
makeChanges({
shouldRender: true,
})
);
onEnterTimeoutRef.current = setTimeout(() => {
updateTransitionState(
makeChanges({
useActiveClass: true,
})
);
startTimeMs.current = new Date().getTime();
}, onEnteringTimeout);
onCallEnterTimeoutRef.current = setTimeout(() => {
// TODO: Actually call this onEnter callback
// onEnter();
startTimeMs.current = null;
}, transitionDurationMs);
}
},
// Heads up! It's important that we track our transitionState's shouldBeMounted, and not the
// one that the user passes in.
[transitionState.shouldBeMounted]
);
return [transitionState.shouldRender, transitionState.useActiveClass];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.