Skip to content

Instantly share code, notes, and snippets.

@tanishqkancharla
Last active January 16, 2024 04:01
Show Gist options
  • Save tanishqkancharla/854962e9f140788e6df5b12eed108978 to your computer and use it in GitHub Desktop.
Save tanishqkancharla/854962e9f140788e6df5b12eed108978 to your computer and use it in GitHub Desktop.
AnimatePresence in React Native
import { unionBy } from "lodash-es";
import React, { ReactElement, useEffect, useState } from "react";
import { Animated, useAnimatedValue } from "react-native";
function diffBy<T>(a: T[], b: T[], key: keyof T) {
const aKeys = a.map((item) => item[key]);
const bKeys = b.map((item) => item[key]);
const added = b.filter((item) => !aKeys.includes(item[key]));
const removed = a.filter((item) => !bKeys.includes(item[key]));
return { added, removed };
}
type AnimatePresenceProps = {
children: React.ReactNode;
};
export const AnimatePresence: React.FC<AnimatePresenceProps> = ({
children,
}) => {
const incomingChildren = React.Children.toArray(children).filter(
(child): child is ReactElement => {
const isValid = React.isValidElement(child);
if (!isValid) {
console.warn(
"AnimatePresence: child is not a valid React element",
child
);
}
return isValid;
}
);
const [currentChildren, setCurrentChildren] = useState(incomingChildren);
const allChildren = unionBy(currentChildren, incomingChildren, "key");
// Diff the incoming children with the current children
const { added: enteringChildren, removed: exitingChildren } = diffBy(
currentChildren,
incomingChildren,
"key"
);
// Render all the children, with the exiting children marked as exiting
return (
<>
{allChildren.map((child) => {
const isExiting = exitingChildren.includes(child);
const isEntering = !isExiting && enteringChildren.includes(child);
return (
<PresenceChild
key={child.key}
isExiting={isExiting}
isEntering={isEntering}
onExitComplete={() => {
setCurrentChildren(
currentChildren.filter((c) => c.key !== child.key)
);
}}
onEnterComplete={() => {
setCurrentChildren(currentChildren.concat(child));
}}
>
{child}
</PresenceChild>
);
})}
</>
);
};
type PresenceChildProps = {
isExiting: boolean;
isEntering: boolean;
onExitComplete: () => void;
onEnterComplete: () => void;
children: React.ReactNode;
};
const PresenceChild: React.FC<PresenceChildProps> = ({
isExiting,
isEntering,
onExitComplete,
onEnterComplete,
children,
}) => {
const opacity = useAnimatedValue(isEntering ? 0 : 1);
const translateY = useAnimatedValue(0);
const [layoutHeight, setLayoutHeight] = useState(0);
const zIndex = isEntering || isExiting ? 0 : 1;
useEffect(() => {
if (isExiting) {
Animated.timing(opacity, {
toValue: 0,
duration: 200,
useNativeDriver: true,
}).start(onExitComplete);
Animated.timing(translateY, {
toValue: -layoutHeight,
duration: 200,
useNativeDriver: true,
}).start();
} else if (isEntering && layoutHeight) {
Animated.timing(opacity, {
toValue: 1,
duration: 200,
useNativeDriver: true,
}).start(onEnterComplete);
Animated.timing(translateY, {
toValue: 0,
duration: 200,
useNativeDriver: true,
}).start();
}
}, [isEntering, isExiting, layoutHeight]);
return (
<Animated.View
style={{
opacity,
transform: [{ translateY }],
zIndex,
top: isEntering && !layoutHeight ? "-100%" : undefined,
}}
onLayout={(event) => {
const newLayoutHeight = event.nativeEvent.layout.height;
if (newLayoutHeight === layoutHeight) return;
translateY.setValue(-newLayoutHeight);
setLayoutHeight(newLayoutHeight);
}}
>
{children}
</Animated.View>
);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment