/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format */ import type {EventSubscription} from '../EventEmitter/NativeEventEmitter'; import type {AnimatedPropsAllowlist} from './nodes/AnimatedProps'; import NativeAnimatedHelper from '../../src/private/animated/NativeAnimatedHelper'; import {useAnimatedPropsMemo} from '../../src/private/animated/useAnimatedPropsMemo'; import * as ReactNativeFeatureFlags from '../../src/private/featureflags/ReactNativeFeatureFlags'; import {isPublicInstance as isFabricPublicInstance} from '../ReactNative/ReactFabricPublicInstance/ReactFabricPublicInstanceUtils'; import useRefEffect from '../Utilities/useRefEffect'; import {AnimatedEvent} from './AnimatedEvent'; import AnimatedNode from './nodes/AnimatedNode'; import AnimatedProps from './nodes/AnimatedProps'; import AnimatedValue from './nodes/AnimatedValue'; import { useCallback, useEffect, useInsertionEffect, useLayoutEffect, useMemo, useReducer, useRef, } from 'react'; type ReducedProps = { ...TProps, collapsable: boolean, ... }; type CallbackRef = T => mixed; type UpdateCallback = () => void; type AnimatedValueListeners = Array<{ propValue: AnimatedValue, listenerId: string, }>; const useMemoOrAnimatedPropsMemo = ReactNativeFeatureFlags.enableAnimatedPropsMemo() ? useAnimatedPropsMemo : useMemo; export default function useAnimatedProps( props: TProps, allowlist?: ?AnimatedPropsAllowlist, ): [ReducedProps, CallbackRef] { const [, scheduleUpdate] = useReducer(count => count + 1, 0); const onUpdateRef = useRef(null); const timerRef = useRef(null); const allowlistIfEnabled = ReactNativeFeatureFlags.enableAnimatedAllowlist() ? allowlist : null; const node = useMemoOrAnimatedPropsMemo( () => new AnimatedProps( props, () => onUpdateRef.current?.(), allowlistIfEnabled, ), [allowlistIfEnabled, props], ); const useNativePropsInFabric = ReactNativeFeatureFlags.shouldUseSetNativePropsInFabric(); const useAnimatedPropsLifecycle = ReactNativeFeatureFlags.useInsertionEffectsForAnimations() ? useAnimatedPropsLifecycle_insertionEffects : useAnimatedPropsLifecycle_layoutEffects; useAnimatedPropsLifecycle(node); // TODO: This "effect" does three things: // // 1) Call `setNativeView`. // 2) Update `onUpdateRef`. // 3) Update listeners for `AnimatedEvent` props. // // Ideally, each of these would be separate "effects" so that they are not // unnecessarily re-run when irrelevant dependencies change. For example, we // should be able to hoist all `AnimatedEvent` props and only do #3 if either // the `AnimatedEvent` props change or `instance` changes. // // But there is no way to transparently compose three separate callback refs, // so we just combine them all into one for now. const refEffect = useCallback( (instance: TInstance) => { // NOTE: This may be called more often than necessary (e.g. when `props` // changes), but `setNativeView` already optimizes for that. node.setNativeView(instance); // NOTE: When using the JS animation driver, this callback is called on // every animation frame. When using the native driver, this callback is // called when the animation completes. onUpdateRef.current = () => { if (process.env.NODE_ENV === 'test') { // Check 1: this is a test. // call `scheduleUpdate` to bypass use of setNativeProps. return scheduleUpdate(); } const isFabricNode = isFabricInstance(instance); if (node.__isNative) { // Check 2: this is an animation driven by native. // In native driven animations, this callback is only called once the animation completes. if (isFabricNode) { // Call `scheduleUpdate` to synchronise Fiber and Shadow tree. // Must not be called in Paper. scheduleUpdate(); } return; } if ( typeof instance !== 'object' || typeof instance?.setNativeProps !== 'function' ) { // Check 3: the instance does not support setNativeProps. Call `scheduleUpdate`. return scheduleUpdate(); } if (!isFabricNode) { // Check 4: this is a paper instance, call setNativeProps. // $FlowIgnore[not-a-function] - Assume it's still a function. // $FlowFixMe[incompatible-use] return instance.setNativeProps(node.__getAnimatedValue()); } if (!useNativePropsInFabric) { // Check 5: setNativeProps are disabled. return scheduleUpdate(); } // This is a Fabric instance and setNativeProps is supported. // $FlowIgnore[not-a-function] - Assume it's still a function. // $FlowFixMe[incompatible-use] instance.setNativeProps(node.__getAnimatedValue()); // Keeping state of Fiber tree and Shadow tree in sync. // // This is done by calling `scheduleUpdate` which will trigger a commit. // However, React commit is not fast enough to drive animations. // This is where setNativeProps comes in handy but the state between // Fiber tree and Shadow tree needs to be kept in sync. // The goal is to call `scheduleUpdate` as little as possible to maintain // performance but frequently enough to keep state in sync. // Debounce is set to 48ms, which is 3 * the duration of a frame. // 3 frames was the highest value where flickering state was not observed. if (timerRef.current != null) { clearTimeout(timerRef.current); } timerRef.current = setTimeout(() => { timerRef.current = null; scheduleUpdate(); }, 48); }; const target = getEventTarget(instance); const events = []; const animatedValueListeners: AnimatedValueListeners = []; for (const propName in props) { // $FlowFixMe[invalid-computed-prop] const propValue = props[propName]; if (propValue instanceof AnimatedEvent && propValue.__isNative) { propValue.__attach(target, propName); events.push([propName, propValue]); // $FlowFixMe[incompatible-call] - the `addListenersToPropsValue` drills down the propValue. addListenersToPropsValue(propValue, animatedValueListeners); } } return () => { onUpdateRef.current = null; for (const [propName, propValue] of events) { propValue.__detach(target, propName); } for (const {propValue, listenerId} of animatedValueListeners) { propValue.removeListener(listenerId); } }; }, [node, useNativePropsInFabric, props], ); const callbackRef = useRefEffect(refEffect); return [reduceAnimatedProps(node, props), callbackRef]; } function reduceAnimatedProps( node: AnimatedProps, props: TProps, ): ReducedProps { // Force `collapsable` to be false so that the native view is not flattened. // Flattened views cannot be accurately referenced by the native driver. return { ...(ReactNativeFeatureFlags.enableAnimatedPropsMemo() ? node.__getValueWithStaticProps(props) : node.__getValue()), collapsable: false, }; } function addListenersToPropsValue( propValue: AnimatedValue, accumulator: AnimatedValueListeners, ) { // propValue can be a scalar value, an array or an object. if (propValue instanceof AnimatedValue) { const listenerId = propValue.addListener(() => {}); accumulator.push({propValue, listenerId}); } else if (Array.isArray(propValue)) { // An array can be an array of scalar values, arrays of arrays, or arrays of objects for (const prop of propValue) { addListenersToPropsValue(prop, accumulator); } } else if (propValue instanceof Object) { addAnimatedValuesListenersToProps(propValue, accumulator); } } function addAnimatedValuesListenersToProps( props: AnimatedNode, accumulator: AnimatedValueListeners, ) { for (const propName in props) { // $FlowFixMe[prop-missing] - This is an object contained in a prop, but we don't know the exact type. const propValue = props[propName]; addListenersToPropsValue(propValue, accumulator); } } /** * Manages the lifecycle of the supplied `AnimatedProps` by invoking `__attach` * and `__detach`. However, this is more complicated because `AnimatedProps` * uses reference counting to determine when to recursively detach its children * nodes. So in order to optimize this, we avoid detaching until the next attach * unless we are unmounting. */ function useAnimatedPropsLifecycle_layoutEffects(node: AnimatedProps): void { const prevNodeRef = useRef(null); const isUnmountingRef = useRef(false); useEffect(() => { // It is ok for multiple components to call `flushQueue` because it noops // if the queue is empty. When multiple animated components are mounted at // the same time. Only first component flushes the queue and the others will noop. NativeAnimatedHelper.API.flushQueue(); let drivenAnimationEndedListener: ?EventSubscription = null; if (node.__isNative) { drivenAnimationEndedListener = NativeAnimatedHelper.nativeEventEmitter.addListener( 'onUserDrivenAnimationEnded', data => { node.update(); }, ); } return () => { drivenAnimationEndedListener?.remove(); }; }); useLayoutEffect(() => { isUnmountingRef.current = false; return () => { isUnmountingRef.current = true; }; }, []); useLayoutEffect(() => { node.__attach(); if (prevNodeRef.current != null) { const prevNode = prevNodeRef.current; // TODO: Stop restoring default values (unless `reset` is called). prevNode.__restoreDefaultValues(); prevNode.__detach(); prevNodeRef.current = null; } return () => { if (isUnmountingRef.current) { // NOTE: Do not restore default values on unmount, see D18197735. node.__detach(); } else { prevNodeRef.current = node; } }; }, [node]); } /** * Manages the lifecycle of the supplied `AnimatedProps` by invoking `__attach` * and `__detach`. However, this is more complicated because `AnimatedProps` * uses reference counting to determine when to recursively detach its children * nodes. So in order to optimize this, we avoid detaching until the next attach * unless we are unmounting. */ function useAnimatedPropsLifecycle_insertionEffects(node: AnimatedProps): void { const prevNodeRef = useRef(null); const isUnmountingRef = useRef(false); useEffect(() => { // It is ok for multiple components to call `flushQueue` because it noops // if the queue is empty. When multiple animated components are mounted at // the same time. Only first component flushes the queue and the others will noop. NativeAnimatedHelper.API.flushQueue(); }); useInsertionEffect(() => { isUnmountingRef.current = false; return () => { isUnmountingRef.current = true; }; }, []); useInsertionEffect(() => { node.__attach(); let drivenAnimationEndedListener: ?EventSubscription = null; if (node.__isNative) { drivenAnimationEndedListener = NativeAnimatedHelper.nativeEventEmitter.addListener( 'onUserDrivenAnimationEnded', data => { node.update(); }, ); } if (prevNodeRef.current != null) { const prevNode = prevNodeRef.current; // TODO: Stop restoring default values (unless `reset` is called). prevNode.__restoreDefaultValues(); prevNode.__detach(); prevNodeRef.current = null; } return () => { if (isUnmountingRef.current) { // NOTE: Do not restore default values on unmount, see D18197735. node.__detach(); } else { prevNodeRef.current = node; } drivenAnimationEndedListener?.remove(); }; }, [node]); } function getEventTarget(instance: TInstance): TInstance { return typeof instance === 'object' && typeof instance?.getScrollableNode === 'function' ? // $FlowFixMe[incompatible-use] - Legacy instance assumptions. instance.getScrollableNode() : instance; } // $FlowFixMe[unclear-type] - Legacy instance assumptions. function isFabricInstance(instance: any): boolean { return ( isFabricPublicInstance(instance) || // Some components have a setNativeProps function but aren't a host component // such as lists like FlatList and SectionList. These should also use // forceUpdate in Fabric since setNativeProps doesn't exist on the underlying // host component. This crazy hack is essentially special casing those lists and // ScrollView itself to use forceUpdate in Fabric. // If these components end up using forwardRef then these hacks can go away // as instance would actually be the underlying host component and the above check // would be sufficient. isFabricPublicInstance(instance?.getNativeScrollRef?.()) || isFabricPublicInstance( instance?.getScrollResponder?.()?.getNativeScrollRef?.(), ) ); }