In the animationSetup variable you can pass in a custom animation object to override the values used for the animation, such as type (decay, timing, spring) and then the setup variables for that type of animation directly matching the Animated API in React Native.
I hope you find it useful. With a bit of work, it could also be modified to work with the web version of react router by switching out the Animated.Views with divs and css animations. I haven't done that as we use NextJS for our web apps.
This is the usePrevious hook used to keep track of location and children of the previous render. It needs to be available to AnimatedSwitch (you may need to adjust the import if your file structure doesn't match mine).
This is the AnimatedSwitch component.
export const ANIMATE_SLIDE = {
previous: anim => ({
position: 'absolute',
transform: [
{
translateX: anim.interpolate({
inputRange: [0, 1],
outputRange: [0, -width],
}),
},
],
}),
new: anim => ({
position: 'absolute',
transform: [
{
translateX: anim.interpolate({
inputRange: [0, 1],
outputRange: [width, 0],
}),
},
],
}),
backPrevious: anim => ({
position: 'absolute',
transform: [
{
translateX: anim.interpolate({
inputRange: [0, 1],
outputRange: [0, width],
}),
},
],
}),
backNew: anim => ({
position: 'absolute',
transform: [
{
translateX: anim.interpolate({
inputRange: [0, 1],
outputRange: [-width, 0],
}),
},
],
}),
};
export const ANIMATE_FADE = {
previous: anim => ({
opacity: anim.interpolate({
inputRange: [0, 0.75],
outputRange: [1, 0],
}),
}),
new: anim => ({
opacity: anim.interpolate({
inputRange: [0, 0.75],
outputRange: [0, 1],
}),
}),
backPrevious: anim => ({
opacity: anim.interpolate({
inputRange: [0, 0.75],
outputRange: [1, 0],
}),
}),
backNew: anim => ({
opacity: anim.interpolate({
inputRange: [0, 0.75],
outputRange: [0, 1],
}),
}),
};
const getPreviousRoute = (exact, prevPath, routes) => {
if (!routes) {
return null;
}
return routes.find(route => {
const match = matchPath(prevPath.pathname, route.props);
if (exact) {
return match != null && match.isExact;
} else {
return match != null;
}
});
};
const renderPreviousRoute = previousRoute => {
if (previousRoute) {
return previousRoute.props.component ? (
React.createElement(previousRoute.props.component)
) : (
<Route render={previousRoute.props.render} />
);
} else {
return null;
}
};
function AnimatedSwitch({
children,
exact,
animationType = ANIMATE_SLIDE,
animationSetup = DEFAULT_ANIMATION_SETUP,
}) {
const [animating, setAnimating] = useState(false);
const [anim] = useState(new Animated.Value(0));
const [previousRoute, setPreviousRoute] = useState();
const location = useLocation();
const history = useHistory();
// we're going to save the previous matching route so we can render
// it when it doesn't actually match the location anymore
const previousLocation = usePrevious(location);
const previousChildren = usePrevious(children);
// now save the animation type for both previous and new views
const [newAnimationStyle, setNewAnimationStyle] = useState();
const [prevAnimationStyle, setPrevAnimationStyle] = useState();
const finishAnimating = () => {
setAnimating(false);
setPreviousRoute();
};
const needsAnimation = previousLocation !== location;
useEffect(() => {
const newPreviousRoute = getPreviousRoute(
exact,
previousLocation,
previousChildren,
);
if (needsAnimation && newPreviousRoute) {
// we were rendering, but now we're heading back up to the parent,
// so we need to save the newPreviousRoute so we can render it
// while the animation is playing
if (history.action === 'POP' || history.action === 'REPLACE') {
setNewAnimationStyle(animationType.backNew(anim));
setPrevAnimationStyle(animationType.backPrevious(anim));
} else {
setNewAnimationStyle(animationType.new(anim));
setPrevAnimationStyle(animationType.previous(anim));
}
setPreviousRoute(newPreviousRoute);
setAnimating(true);
}
}, [
location,
needsAnimation,
animationType,
previousChildren,
previousLocation,
history.action,
anim,
exact,
]);
useEffect(() => {
if (animating) {
switch (animationSetup.type) {
case 'decay':
Animated.decay(anim, animationSetup.init).start(finishAnimating);
break;
case 'timing':
Animated.timing(anim, animationSetup.init).start(finishAnimating);
break;
case 'spring':
default:
Animated.spring(anim, animationSetup.init).start(finishAnimating);
}
}
}, [animating, anim, animationSetup]);
// Need to render the previous route for the time between not animating and animating
if (needsAnimation && !animating) {
const tempPreviousRoute = getPreviousRoute(
exact,
previousLocation,
previousChildren,
);
if (tempPreviousRoute) {
const prevRouteComp = renderPreviousRoute(tempPreviousRoute);
return prevRouteComp;
} else {
return null;
}
}
if (animating) {
// Animate both the previous route and the new route at the same time,
// then change them to the new route based on the animation type, so either slide or fade or spring
const prevRouteComp = renderPreviousRoute(previousRoute);
return (
<View>
<Animated.View key="newView" style={newAnimationStyle}>
<Switch>{children}</Switch>
</Animated.View>
<Animated.View key="prevView" style={prevAnimationStyle}>
<Switch>{prevRouteComp}</Switch>
</Animated.View>
</View>
);
} else {
// Just animate the actual route from the location
return (
<View>
<Animated.View key="newView">
<Switch>{children}</Switch>
</Animated.View>
</View>
);
}
}
export default AnimatedSwitch;