diff --git a/packages/react-strict-dom/src/native/modules/useStyleTransition.js b/packages/react-strict-dom/src/native/modules/useStyleTransition.js index 82d6fe7d..69178d17 100644 --- a/packages/react-strict-dom/src/native/modules/useStyleTransition.js +++ b/packages/react-strict-dom/src/native/modules/useStyleTransition.js @@ -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'; @@ -29,6 +29,13 @@ type TransitionMetadata = $ReadOnly<{ shouldUseNativeDriver: boolean }>; +type AnimatedConfig = { + start: () => void, + dispose: () => void, + value: Animated.Value, + referenceCount: number +}; + const INPUT_RANGE: $ReadOnlyArray = [0, 1]; function isNumber(num: mixed): num is number { @@ -260,6 +267,53 @@ function getAnimation( }); } +const animatedConfigs = new Map(); + +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, @@ -292,7 +346,7 @@ export function useStyleTransition(style: ReactNativeStyle): ReactNativeStyle { undefined ); - const [animatedValue, setAnimatedValue] = useState( + const [animatedConfig, setAnimatedConfig] = useState( undefined ); @@ -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 && @@ -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, [property, value]) => {