Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 80 additions & 28 deletions packages/react-strict-dom/src/native/modules/useStyleTransition.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type {
ReactNativeTransform
} from '../../types/renderer.native';

import { useEffect, useRef, useState } from 'react';
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { errorMsg, warnMsg } from '../../shared/logUtils';
import { Animated, Easing } from 'react-native';

Expand All @@ -29,6 +29,13 @@ type TransitionMetadata = $ReadOnly<{
shouldUseNativeDriver: boolean
}>;

type AnimatedConfig = {
start: () => void,
dispose: () => void,
value: Animated.Value,
referenceCount: number
};

const INPUT_RANGE: $ReadOnlyArray<number> = [0, 1];

function isNumber(num: mixed): num is number {
Expand Down Expand Up @@ -260,6 +267,53 @@ function getAnimation(
});
}

const animatedConfigs = new Map<string, AnimatedConfig>();

function getOrCreateAnimatedConfig(transitionMetadata: TransitionMetadata) {
const key = JSON.stringify(transitionMetadata);

const animatedConfig = animatedConfigs.get(key);
if (animatedConfig != null) {
animatedConfig.referenceCount++;

return animatedConfig;
}

const animatedValue = new Animated.Value(0);
let hasStarted = false;
let animation;
const newAnimatedConfig = {
referenceCount: 1,
value: animatedValue,
start: () => {
if (hasStarted) {
return;
}
hasStarted = true;
const { delay, duration, timingFunction, shouldUseNativeDriver } =
transitionMetadata;
animation = Animated.sequence([
Animated.delay(delay),
getAnimation(
animatedValue,
duration,
timingFunction,
shouldUseNativeDriver
)
]);
animation.start();
},
dispose: () => {
if (--newAnimatedConfig.referenceCount === 0) {
animation?.stop();
}
}
};
animatedConfigs.set(key, newAnimatedConfig);

return newAnimatedConfig;
}

export function useStyleTransition(style: ReactNativeStyle): ReactNativeStyle {
const {
transitionDelay: _delay,
Expand Down Expand Up @@ -292,7 +346,7 @@ export function useStyleTransition(style: ReactNativeStyle): ReactNativeStyle {
undefined
);

const [animatedValue, setAnimatedValue] = useState<Animated.Value | void>(
const [animatedConfig, setAnimatedConfig] = useState<AnimatedConfig | void>(
undefined
);

Expand Down Expand Up @@ -321,28 +375,32 @@ export function useStyleTransition(style: ReactNativeStyle): ReactNativeStyle {
]);

// effect to trigger a transition
// REMEMBER: it is super important that this effect's dependency array **only** contains the animated value
// REMEMBER: it is super important that this effect's dependency array **only** contains the animated config
useEffect(() => {
if (animatedValue !== undefined) {
const { delay, duration, timingFunction, shouldUseNativeDriver } =
transitionMetadataRef.current;
if (animatedConfig == null) {
return;
}
animatedConfig.start();
animatedConfigs.clear();
return () => {
animatedConfig.dispose();
};
}, [animatedConfig]);

const animation = Animated.sequence([
Animated.delay(delay),
getAnimation(
animatedValue,
duration,
timingFunction,
shouldUseNativeDriver
)
]);
animation.start();
const transitionStyleHasChangedResult = transitionStyleHasChanged(
transitionStyle,
currentStyle
);

return () => {
animation.stop();
};
useLayoutEffect(() => {
if (transitionStyleHasChangedResult) {
setCurrentStyle(style);
setPreviousStyle(currentStyle);
setAnimatedConfig(
getOrCreateAnimatedConfig(transitionMetadataRef.current)
);
}
}, [animatedValue]);
}, [currentStyle, style, transitionStyleHasChangedResult]);

if (
_delay == null &&
Expand All @@ -354,18 +412,12 @@ export function useStyleTransition(style: ReactNativeStyle): ReactNativeStyle {
return style;
}

if (transitionStyleHasChanged(transitionStyle, currentStyle)) {
setCurrentStyle(style);
setPreviousStyle(currentStyle);
setAnimatedValue(new Animated.Value(0));
// This commit will be thrown away due to the above state setters so we can bail out early
return style;
}

if (transitionStyle === undefined) {
return style;
}

const animatedValue = animatedConfig?.value;

const outputAnimatedStyle: AnimatedStyle = Object.entries(
transitionStyle
).reduce<AnimatedStyle>((animatedStyle, [property, value]) => {
Expand Down