mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Animated: Minimize AnimatedProps Invalidation (#46386)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46386 Currently, `useAnimatedProps` attempts (and fails) to memoize the `AnimatedProps` instance by using `props` in the dependency array: https://www.internalfb.com/code/fbsource/[de6bf108ae11fad0e8516617cde6f0cf3152d129]/xplat/js/react-native-github/packages/react-native/Libraries/Animated/useAnimatedProps.js?lines=56-59 However, `props` is very easily invalidated whenever the component is updated by its parent. The only time when it will not be invalidated is if the component that directly uses this hook — `createAnimatedComponent`'s return value — updates state… which never happens. This changes `useAnimatedProps` so that we memoize `AnimatedProps` using only the nested property values that actually require a new `AnimatedProps` object to be created, which are `AnimatedNode` subclasses. A minor detail is that in order for `AnimatedProps.prototype.__getValue` to continue working, we must supply `props` when we're actualizing the `AnimatedNode` instances into real values. This is accomplished by introducing a new method to select `AnimatedNode` subclasses: `__getValueFromStatic{Props,Object,Style,Transforms}` Changelog: [General][Added] - Created an experimental optimization to improve memoization within `Animated` to improve product performance Reviewed By: javache Differential Revision: D61997128 fbshipit-source-id: 2ca3cfb1038ba9a0abd82904acbc5cb4eb41a45d
This commit is contained in:
committed by
Facebook GitHub Bot
parent
401f735e35
commit
dc84d00db6
@@ -19,9 +19,11 @@ import * as React from 'react';
|
||||
|
||||
const MAX_DEPTH = 5;
|
||||
|
||||
/* $FlowIssue[incompatible-type-guard] - Flow does not know that the prototype
|
||||
and ReactElement checks preserve the type refinement of `value`. */
|
||||
function isPlainObject(value: mixed): value is $ReadOnly<{[string]: mixed}> {
|
||||
export function isPlainObject(
|
||||
value: mixed,
|
||||
/* $FlowIssue[incompatible-type-guard] - Flow does not know that the prototype
|
||||
and ReactElement checks preserve the type refinement of `value`. */
|
||||
): value is $ReadOnly<{[string]: mixed}> {
|
||||
return (
|
||||
value !== null &&
|
||||
typeof value === 'object' &&
|
||||
@@ -109,6 +111,14 @@ export default class AnimatedObject extends AnimatedWithChildren {
|
||||
});
|
||||
}
|
||||
|
||||
__getValueWithStaticObject(staticObject: mixed): any {
|
||||
const nodes = this.#nodes;
|
||||
let index = 0;
|
||||
// NOTE: We can depend on `this._value` and `staticObject` sharing a
|
||||
// structure because of `useAnimatedPropsMemo`.
|
||||
return mapAnimatedNodes(staticObject, () => nodes[index++].__getValue());
|
||||
}
|
||||
|
||||
__getAnimatedValue(): any {
|
||||
return mapAnimatedNodes(this._value, node => {
|
||||
return node.__getAnimatedValue();
|
||||
|
||||
@@ -113,6 +113,31 @@ export default class AnimatedProps extends AnimatedNode {
|
||||
return props;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new `props` object that contains the same props as the supplied
|
||||
* `staticProps` object, except with animated nodes for any props that were
|
||||
* created by this `AnimatedProps` instance.
|
||||
*/
|
||||
__getValueWithStaticProps(staticProps: Object): Object {
|
||||
const props: {[string]: mixed} = {...staticProps};
|
||||
|
||||
const keys = Object.keys(staticProps);
|
||||
for (let ii = 0, length = keys.length; ii < length; ii++) {
|
||||
const key = keys[ii];
|
||||
const maybeNode = this.#props[key];
|
||||
|
||||
if (key === 'style' && maybeNode instanceof AnimatedStyle) {
|
||||
props[key] = maybeNode.__getValueWithStaticStyle(staticProps.style);
|
||||
} else if (maybeNode instanceof AnimatedNode) {
|
||||
props[key] = maybeNode.__getValue();
|
||||
} else if (maybeNode instanceof AnimatedEvent) {
|
||||
props[key] = maybeNode.__getHandler();
|
||||
}
|
||||
}
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
__getAnimatedValue(): Object {
|
||||
const props: {[string]: mixed} = {};
|
||||
|
||||
|
||||
@@ -138,6 +138,43 @@ export default class AnimatedStyle extends AnimatedWithChildren {
|
||||
return Platform.OS === 'web' ? [this.#inputStyle, style] : style;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new `style` object that contains the same style properties as
|
||||
* the supplied `staticStyle` object, except with animated nodes for any
|
||||
* style properties that were created by this `AnimatedStyle` instance.
|
||||
*/
|
||||
__getValueWithStaticStyle(staticStyle: Object): Object | Array<Object> {
|
||||
const flatStaticStyle = flattenStyle(staticStyle);
|
||||
const style: {[string]: mixed} =
|
||||
flatStaticStyle == null
|
||||
? {}
|
||||
: flatStaticStyle === staticStyle
|
||||
? // Copy the input style, since we'll mutate it below.
|
||||
{...flatStaticStyle}
|
||||
: // Reuse `flatStaticStyle` if it is a newly created object.
|
||||
flatStaticStyle;
|
||||
|
||||
const keys = Object.keys(style);
|
||||
for (let ii = 0, length = keys.length; ii < length; ii++) {
|
||||
const key = keys[ii];
|
||||
const maybeNode = this.#style[key];
|
||||
|
||||
if (key === 'transform' && maybeNode instanceof AnimatedTransform) {
|
||||
style[key] = maybeNode.__getValueWithStaticTransforms(
|
||||
// NOTE: This check should not be necessary, but the types are not
|
||||
// enforced as of this writing.
|
||||
Array.isArray(style[key]) ? style[key] : [],
|
||||
);
|
||||
} else if (maybeNode instanceof AnimatedObject) {
|
||||
style[key] = maybeNode.__getValueWithStaticObject(style[key]);
|
||||
} else if (maybeNode instanceof AnimatedNode) {
|
||||
style[key] = maybeNode.__getValue();
|
||||
}
|
||||
}
|
||||
|
||||
return Platform.OS === 'web' ? [this.#inputStyle, style] : style;
|
||||
}
|
||||
|
||||
__getAnimatedValue(): Object {
|
||||
const style: {[string]: mixed} = {};
|
||||
|
||||
|
||||
@@ -91,6 +91,18 @@ export default class AnimatedTransform extends AnimatedWithChildren {
|
||||
);
|
||||
}
|
||||
|
||||
__getValueWithStaticTransforms(
|
||||
staticTransforms: $ReadOnlyArray<Object>,
|
||||
): $ReadOnlyArray<Object> {
|
||||
const values = [];
|
||||
mapTransforms(this._transforms, node => {
|
||||
values.push(node.__getValue());
|
||||
});
|
||||
// NOTE: We can depend on `this._transforms` and `staticTransforms` sharing
|
||||
// a structure because of `useAnimatedPropsMemo`.
|
||||
return mapTransforms(staticTransforms, () => values.shift());
|
||||
}
|
||||
|
||||
__getAnimatedValue(): $ReadOnlyArray<Transform<any>> {
|
||||
return mapTransforms(this._transforms, animatedNode =>
|
||||
animatedNode.__getAnimatedValue(),
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
useReducer,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import {useAnimatedPropsMemo} from '../../src/private/animated/useAnimatedPropsMemo';
|
||||
|
||||
type ReducedProps<TProps> = {
|
||||
...TProps,
|
||||
@@ -41,23 +42,24 @@ type AnimatedValueListeners = Array<{
|
||||
listenerId: string,
|
||||
}>;
|
||||
|
||||
const useMemoOrAnimatedPropsMemo =
|
||||
ReactNativeFeatureFlags.enableAnimatedPropsMemo()
|
||||
? useAnimatedPropsMemo
|
||||
: useMemo;
|
||||
|
||||
export default function useAnimatedProps<TProps: {...}, TInstance>(
|
||||
props: TProps,
|
||||
allowlist?: ?AnimatedPropsAllowlist,
|
||||
): [ReducedProps<TProps>, CallbackRef<TInstance | null>] {
|
||||
const [, scheduleUpdate] = useReducer<number, void>(count => count + 1, 0);
|
||||
const onUpdateRef = useRef<?() => void>(null);
|
||||
const onUpdateRef = useRef<(() => void) | null>(null);
|
||||
const timerRef = useRef<TimeoutID | null>(null);
|
||||
|
||||
const allowlistIfEnabled = ReactNativeFeatureFlags.enableAnimatedAllowlist()
|
||||
? allowlist
|
||||
: null;
|
||||
|
||||
// TODO: Only invalidate `node` if animated props or `style` change. In the
|
||||
// previous implementation, we permitted `style` to override props with the
|
||||
// same name property name as styles, so we can probably continue doing that.
|
||||
// The ordering of other props *should* not matter.
|
||||
const node = useMemo(
|
||||
const node = useMemoOrAnimatedPropsMemo(
|
||||
() =>
|
||||
new AnimatedProps(
|
||||
props,
|
||||
@@ -66,6 +68,7 @@ export default function useAnimatedProps<TProps: {...}, TInstance>(
|
||||
),
|
||||
[allowlistIfEnabled, props],
|
||||
);
|
||||
|
||||
const useNativePropsInFabric =
|
||||
ReactNativeFeatureFlags.shouldUseSetNativePropsInFabric();
|
||||
const useSetNativePropsInNativeAnimationsInFabric =
|
||||
@@ -205,14 +208,19 @@ export default function useAnimatedProps<TProps: {...}, TInstance>(
|
||||
);
|
||||
const callbackRef = useRefEffect<TInstance>(refEffect);
|
||||
|
||||
return [reduceAnimatedProps<TProps>(node), callbackRef];
|
||||
return [reduceAnimatedProps<TProps>(node, props), callbackRef];
|
||||
}
|
||||
|
||||
function reduceAnimatedProps<TProps>(node: AnimatedNode): ReducedProps<TProps> {
|
||||
function reduceAnimatedProps<TProps>(
|
||||
node: AnimatedProps,
|
||||
props: TProps,
|
||||
): ReducedProps<TProps> {
|
||||
// Force `collapsable` to be false so that the native view is not flattened.
|
||||
// Flattened views cannot be accurately referenced by the native driver.
|
||||
return {
|
||||
...node.__getValue(),
|
||||
...(ReactNativeFeatureFlags.enableAnimatedPropsMemo()
|
||||
? node.__getValueWithStaticProps(props)
|
||||
: node.__getValue()),
|
||||
collapsable: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -938,11 +938,15 @@ exports[`public API should not change unintentionally Libraries/Animated/nodes/A
|
||||
`;
|
||||
|
||||
exports[`public API should not change unintentionally Libraries/Animated/nodes/AnimatedObject.js 1`] = `
|
||||
"declare export default class AnimatedObject extends AnimatedWithChildren {
|
||||
"declare export function isPlainObject(
|
||||
value: mixed
|
||||
): value is $ReadOnly<{ [string]: mixed }>;
|
||||
declare export default class AnimatedObject extends AnimatedWithChildren {
|
||||
_value: mixed;
|
||||
static from(value: mixed): ?AnimatedObject;
|
||||
constructor(nodes: $ReadOnlyArray<AnimatedNode>, value: mixed): void;
|
||||
__getValue(): any;
|
||||
__getValueWithStaticObject(staticObject: mixed): any;
|
||||
__getAnimatedValue(): any;
|
||||
__attach(): void;
|
||||
__detach(): void;
|
||||
@@ -964,6 +968,7 @@ declare export default class AnimatedProps extends AnimatedNode {
|
||||
allowlist?: ?AnimatedPropsAllowlist
|
||||
): void;
|
||||
__getValue(): Object;
|
||||
__getValueWithStaticProps(staticProps: Object): Object;
|
||||
__getAnimatedValue(): Object;
|
||||
__attach(): void;
|
||||
__detach(): void;
|
||||
@@ -992,6 +997,7 @@ declare export default class AnimatedStyle extends AnimatedWithChildren {
|
||||
inputStyle: any
|
||||
): void;
|
||||
__getValue(): Object | Array<Object>;
|
||||
__getValueWithStaticStyle(staticStyle: Object): Object | Array<Object>;
|
||||
__getAnimatedValue(): Object;
|
||||
__attach(): void;
|
||||
__detach(): void;
|
||||
@@ -1061,6 +1067,9 @@ declare export default class AnimatedTransform extends AnimatedWithChildren {
|
||||
): void;
|
||||
__makeNative(platformConfig: ?PlatformConfig): void;
|
||||
__getValue(): $ReadOnlyArray<Transform<any>>;
|
||||
__getValueWithStaticTransforms(
|
||||
staticTransforms: $ReadOnlyArray<Object>
|
||||
): $ReadOnlyArray<Object>;
|
||||
__getAnimatedValue(): $ReadOnlyArray<Transform<any>>;
|
||||
__attach(): void;
|
||||
__detach(): void;
|
||||
|
||||
@@ -298,6 +298,11 @@ const definitions: FeatureFlagDefinitions = {
|
||||
defaultValue: false,
|
||||
description: 'Enables Animated to skip non-allowlisted props and styles.',
|
||||
},
|
||||
enableAnimatedPropsMemo: {
|
||||
defaultValue: false,
|
||||
description:
|
||||
'Enables Animated to analyze props to minimize invalidating `AnimatedProps`.',
|
||||
},
|
||||
enableOptimisedVirtualizedCells: {
|
||||
defaultValue: false,
|
||||
description:
|
||||
|
||||
+138
-6
@@ -15,9 +15,19 @@ import {createRef} from 'react';
|
||||
const {create, unmount, update} = require('../../../../jest/renderer');
|
||||
|
||||
describe('Native Animated', () => {
|
||||
let Animated;
|
||||
let NativeAnimatedHelper;
|
||||
let NativeAnimatedModule;
|
||||
function importModules() {
|
||||
return {
|
||||
get Animated() {
|
||||
return require('../../../../Libraries/Animated/Animated').default;
|
||||
},
|
||||
get NativeAnimatedHelper() {
|
||||
return require('../NativeAnimatedHelper').default;
|
||||
},
|
||||
get ReactNativeFeatureFlags() {
|
||||
return require('../../featureflags/ReactNativeFeatureFlags');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
@@ -60,13 +70,12 @@ describe('Native Animated', () => {
|
||||
stopAnimation: jest.fn(),
|
||||
stopListeningToAnimatedNodeValue: jest.fn(),
|
||||
});
|
||||
|
||||
Animated = require('../../../../Libraries/Animated/Animated').default;
|
||||
NativeAnimatedHelper = require('../NativeAnimatedHelper').default;
|
||||
});
|
||||
|
||||
describe('Animated Value', () => {
|
||||
it('proxies `setValue` correctly', async () => {
|
||||
const {Animated} = importModules();
|
||||
|
||||
const opacity = new Animated.Value(0);
|
||||
const ref = React.createRef(null);
|
||||
|
||||
@@ -91,6 +100,8 @@ describe('Native Animated', () => {
|
||||
});
|
||||
|
||||
it('should set offset', async () => {
|
||||
const {Animated} = importModules();
|
||||
|
||||
const opacity = new Animated.Value(0);
|
||||
opacity.setOffset(10);
|
||||
opacity.__makeNative();
|
||||
@@ -109,6 +120,8 @@ describe('Native Animated', () => {
|
||||
});
|
||||
|
||||
it('should flatten offset', async () => {
|
||||
const {Animated} = importModules();
|
||||
|
||||
const opacity = new Animated.Value(0);
|
||||
opacity.__makeNative();
|
||||
|
||||
@@ -125,6 +138,8 @@ describe('Native Animated', () => {
|
||||
});
|
||||
|
||||
it('should save value on unmount', async () => {
|
||||
const {Animated} = importModules();
|
||||
|
||||
NativeAnimatedModule.getValue = jest.fn((tag, saveCallback) => {
|
||||
saveCallback(1);
|
||||
});
|
||||
@@ -145,6 +160,8 @@ describe('Native Animated', () => {
|
||||
});
|
||||
|
||||
it('should deduct offset when saving value on unmount', async () => {
|
||||
const {Animated} = importModules();
|
||||
|
||||
NativeAnimatedModule.getValue = jest.fn((tag, saveCallback) => {
|
||||
// Assume current raw value of value node is 0.5, the NativeAnimated
|
||||
// getValue API returns the sum of raw value and offset, so return 1.
|
||||
@@ -167,6 +184,8 @@ describe('Native Animated', () => {
|
||||
});
|
||||
|
||||
it('should extract offset', async () => {
|
||||
const {Animated} = importModules();
|
||||
|
||||
const opacity = new Animated.Value(0);
|
||||
opacity.__makeNative();
|
||||
|
||||
@@ -185,6 +204,8 @@ describe('Native Animated', () => {
|
||||
|
||||
describe('Animated Listeners', () => {
|
||||
it('should get updates', () => {
|
||||
const {Animated, NativeAnimatedHelper} = importModules();
|
||||
|
||||
const value1 = new Animated.Value(0);
|
||||
value1.__makeNative();
|
||||
const listener = jest.fn();
|
||||
@@ -223,6 +244,8 @@ describe('Native Animated', () => {
|
||||
});
|
||||
|
||||
it('should removeAll', () => {
|
||||
const {Animated, NativeAnimatedHelper} = importModules();
|
||||
|
||||
const value1 = new Animated.Value(0);
|
||||
value1.__makeNative();
|
||||
const listener = jest.fn();
|
||||
@@ -253,6 +276,8 @@ describe('Native Animated', () => {
|
||||
|
||||
describe('Animated Events', () => {
|
||||
it('should map events', async () => {
|
||||
const {Animated} = importModules();
|
||||
|
||||
const value = new Animated.Value(0);
|
||||
value.__makeNative();
|
||||
const event = Animated.event([{nativeEvent: {state: {foo: value}}}], {
|
||||
@@ -281,6 +306,8 @@ describe('Native Animated', () => {
|
||||
});
|
||||
|
||||
it('should map AnimatedValueXY', async () => {
|
||||
const {Animated} = importModules();
|
||||
|
||||
const value = new Animated.ValueXY({x: 0, y: 0});
|
||||
value.__makeNative();
|
||||
const event = Animated.event([{nativeEvent: {state: value}}], {
|
||||
@@ -299,6 +326,8 @@ describe('Native Animated', () => {
|
||||
});
|
||||
|
||||
it('should throw on invalid event path', async () => {
|
||||
const {Animated} = importModules();
|
||||
|
||||
const value = new Animated.Value(0);
|
||||
value.__makeNative();
|
||||
const event = Animated.event([{notNativeEvent: {foo: value}}], {
|
||||
@@ -323,6 +352,8 @@ describe('Native Animated', () => {
|
||||
});
|
||||
|
||||
it('should call listeners', () => {
|
||||
const {Animated} = importModules();
|
||||
|
||||
const value = new Animated.Value(0);
|
||||
value.__makeNative();
|
||||
const listener = jest.fn();
|
||||
@@ -339,6 +370,8 @@ describe('Native Animated', () => {
|
||||
|
||||
describe('Animated Graph', () => {
|
||||
it('creates and detaches nodes', async () => {
|
||||
const {Animated} = importModules();
|
||||
|
||||
const opacity = new Animated.Value(0);
|
||||
const root = await create(<Animated.View style={{opacity}} />);
|
||||
|
||||
@@ -379,6 +412,8 @@ describe('Native Animated', () => {
|
||||
});
|
||||
|
||||
it('sends a valid description for value, style and props nodes', async () => {
|
||||
const {Animated} = importModules();
|
||||
|
||||
const opacity = new Animated.Value(0);
|
||||
await create(<Animated.View style={{opacity}} />);
|
||||
|
||||
@@ -403,6 +438,8 @@ describe('Native Animated', () => {
|
||||
});
|
||||
|
||||
it('sends a valid graph description for Animated.add nodes', async () => {
|
||||
const {Animated} = importModules();
|
||||
|
||||
const first = new Animated.Value(1);
|
||||
const second = new Animated.Value(2);
|
||||
first.__makeNative();
|
||||
@@ -447,6 +484,8 @@ describe('Native Animated', () => {
|
||||
});
|
||||
|
||||
it('sends a valid graph description for Animated.subtract nodes', async () => {
|
||||
const {Animated} = importModules();
|
||||
|
||||
const first = new Animated.Value(2);
|
||||
const second = new Animated.Value(1);
|
||||
first.__makeNative();
|
||||
@@ -491,6 +530,8 @@ describe('Native Animated', () => {
|
||||
});
|
||||
|
||||
it('sends a valid graph description for Animated.multiply nodes', async () => {
|
||||
const {Animated} = importModules();
|
||||
|
||||
const first = new Animated.Value(2);
|
||||
const second = new Animated.Value(1);
|
||||
first.__makeNative();
|
||||
@@ -535,6 +576,8 @@ describe('Native Animated', () => {
|
||||
});
|
||||
|
||||
it('sends a valid graph description for Animated.divide nodes', async () => {
|
||||
const {Animated} = importModules();
|
||||
|
||||
const first = new Animated.Value(4);
|
||||
const second = new Animated.Value(2);
|
||||
first.__makeNative();
|
||||
@@ -579,6 +622,8 @@ describe('Native Animated', () => {
|
||||
});
|
||||
|
||||
it('sends a valid graph description for Animated.modulo nodes', async () => {
|
||||
const {Animated} = importModules();
|
||||
|
||||
const value = new Animated.Value(4);
|
||||
value.__makeNative();
|
||||
|
||||
@@ -613,6 +658,8 @@ describe('Native Animated', () => {
|
||||
});
|
||||
|
||||
it('sends a valid graph description for interpolate() nodes', async () => {
|
||||
const {Animated} = importModules();
|
||||
|
||||
const value = new Animated.Value(10);
|
||||
value.__makeNative();
|
||||
|
||||
@@ -657,6 +704,8 @@ describe('Native Animated', () => {
|
||||
});
|
||||
|
||||
it('sends a valid graph description for transform nodes', async () => {
|
||||
const {Animated} = importModules();
|
||||
|
||||
const translateX = new Animated.Value(0);
|
||||
translateX.__makeNative();
|
||||
|
||||
@@ -685,6 +734,8 @@ describe('Native Animated', () => {
|
||||
});
|
||||
|
||||
it('sends create operations before connect operations for multiple animated style props', async () => {
|
||||
const {Animated} = importModules();
|
||||
|
||||
const opacity = new Animated.Value(0);
|
||||
const borderRadius = new Animated.Value(0);
|
||||
await create(<Animated.View style={{borderRadius, opacity}} />);
|
||||
@@ -779,6 +830,8 @@ describe('Native Animated', () => {
|
||||
});
|
||||
|
||||
it('sends create operations before connect operations for multiple animated transform props', async () => {
|
||||
const {Animated} = importModules();
|
||||
|
||||
const translateX = new Animated.Value(0);
|
||||
const translateY = new Animated.Value(0);
|
||||
await create(
|
||||
@@ -901,6 +954,8 @@ describe('Native Animated', () => {
|
||||
});
|
||||
|
||||
it('sends create operations before connect operations for multiple animated props', async () => {
|
||||
const {Animated} = importModules();
|
||||
|
||||
const propA = new Animated.Value(0);
|
||||
const propB = new Animated.Value(0);
|
||||
await create(<Animated.View propA={propA} propB={propB} />);
|
||||
@@ -973,6 +1028,8 @@ describe('Native Animated', () => {
|
||||
});
|
||||
|
||||
it('sends a valid graph description for Animated.diffClamp nodes', async () => {
|
||||
const {Animated} = importModules();
|
||||
|
||||
const value = new Animated.Value(2);
|
||||
value.__makeNative();
|
||||
|
||||
@@ -1007,6 +1064,8 @@ describe('Native Animated', () => {
|
||||
});
|
||||
|
||||
it("doesn't call into native API if useNativeDriver is set to false", async () => {
|
||||
const {Animated} = importModules();
|
||||
|
||||
const opacity = new Animated.Value(0);
|
||||
|
||||
const root = await create(<Animated.View style={{opacity}} />);
|
||||
@@ -1023,6 +1082,8 @@ describe('Native Animated', () => {
|
||||
});
|
||||
|
||||
it('fails when trying to run non-native animation on native node', async () => {
|
||||
const {Animated} = importModules();
|
||||
|
||||
const opacity = new Animated.Value(0);
|
||||
const ref = React.createRef(null);
|
||||
|
||||
@@ -1053,6 +1114,8 @@ describe('Native Animated', () => {
|
||||
});
|
||||
|
||||
it('fails for unsupported styles', async () => {
|
||||
const {Animated} = importModules();
|
||||
|
||||
const left = new Animated.Value(0);
|
||||
|
||||
await create(<Animated.View style={{left}} />);
|
||||
@@ -1073,6 +1136,8 @@ describe('Native Animated', () => {
|
||||
});
|
||||
|
||||
it('works for any `static` props and styles', async () => {
|
||||
const {Animated} = importModules();
|
||||
|
||||
// Passing "unsupported" props should work just fine as long as they are not animated
|
||||
const opacity = new Animated.Value(0);
|
||||
opacity.__makeNative();
|
||||
@@ -1097,6 +1162,8 @@ describe('Native Animated', () => {
|
||||
|
||||
describe('Animations', () => {
|
||||
it('sends a valid timing animation description', () => {
|
||||
const {Animated} = importModules();
|
||||
|
||||
const anim = new Animated.Value(0);
|
||||
Animated.timing(anim, {
|
||||
toValue: 10,
|
||||
@@ -1118,6 +1185,8 @@ describe('Native Animated', () => {
|
||||
});
|
||||
|
||||
it('sends a valid spring animation description', () => {
|
||||
const {Animated} = importModules();
|
||||
|
||||
const anim = new Animated.Value(0);
|
||||
Animated.spring(anim, {
|
||||
toValue: 10,
|
||||
@@ -1194,6 +1263,8 @@ describe('Native Animated', () => {
|
||||
});
|
||||
|
||||
it('sends a valid decay animation description', () => {
|
||||
const {Animated} = importModules();
|
||||
|
||||
const anim = new Animated.Value(0);
|
||||
Animated.decay(anim, {
|
||||
velocity: 10,
|
||||
@@ -1210,6 +1281,8 @@ describe('Native Animated', () => {
|
||||
});
|
||||
|
||||
it('works with Animated.loop', () => {
|
||||
const {Animated} = importModules();
|
||||
|
||||
const anim = new Animated.Value(0);
|
||||
Animated.loop(
|
||||
Animated.decay(anim, {
|
||||
@@ -1229,6 +1302,8 @@ describe('Native Animated', () => {
|
||||
});
|
||||
|
||||
it('sends stopAnimation command to native', () => {
|
||||
const {Animated} = importModules();
|
||||
|
||||
const value = new Animated.Value(0);
|
||||
const animation = Animated.timing(value, {
|
||||
toValue: 10,
|
||||
@@ -1256,6 +1331,8 @@ describe('Native Animated', () => {
|
||||
});
|
||||
|
||||
it('calls stopAnimation callback with native value', () => {
|
||||
const {Animated} = importModules();
|
||||
|
||||
NativeAnimatedModule.getValue = jest.fn((tag, saveCallback) => {
|
||||
saveCallback(1);
|
||||
});
|
||||
@@ -1281,7 +1358,58 @@ describe('Native Animated', () => {
|
||||
});
|
||||
|
||||
describe('Animated Components', () => {
|
||||
it('preserves current values on update and unmount', async () => {
|
||||
const {ReactNativeFeatureFlags} = importModules();
|
||||
ReactNativeFeatureFlags.override({enableAnimatedPropsMemo: () => true});
|
||||
|
||||
const {Animated} = importModules();
|
||||
|
||||
const opacity = new Animated.Value(0);
|
||||
opacity.__makeNative();
|
||||
|
||||
const root = await create(<Animated.View style={{opacity}} />);
|
||||
expect(NativeAnimatedModule.restoreDefaultValues).not.toHaveBeenCalled();
|
||||
|
||||
await update(root, <Animated.View style={{opacity}} />);
|
||||
expect(NativeAnimatedModule.restoreDefaultValues).not.toHaveBeenCalled();
|
||||
|
||||
await unmount(root);
|
||||
// Make sure it doesn't get called on unmount.
|
||||
expect(NativeAnimatedModule.restoreDefaultValues).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('restores defaults when receiving new animated values', async () => {
|
||||
const {ReactNativeFeatureFlags} = importModules();
|
||||
ReactNativeFeatureFlags.override({enableAnimatedPropsMemo: () => true});
|
||||
|
||||
const {Animated} = importModules();
|
||||
|
||||
const opacityA = new Animated.Value(0);
|
||||
const opacityB = new Animated.Value(0);
|
||||
opacityA.__makeNative();
|
||||
opacityB.__makeNative();
|
||||
|
||||
const root = await create(<Animated.View style={{opacity: opacityA}} />);
|
||||
expect(NativeAnimatedModule.restoreDefaultValues).not.toHaveBeenCalled();
|
||||
|
||||
await update(root, <Animated.View style={{opacity: opacityB}} />);
|
||||
expect(NativeAnimatedModule.restoreDefaultValues).toHaveBeenCalledTimes(
|
||||
1,
|
||||
);
|
||||
|
||||
await unmount(root);
|
||||
// Make sure it doesn't get called on unmount.
|
||||
expect(NativeAnimatedModule.restoreDefaultValues).toHaveBeenCalledTimes(
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
it('should restore default values on prop updates only', async () => {
|
||||
const {ReactNativeFeatureFlags} = importModules();
|
||||
ReactNativeFeatureFlags.override({enableAnimatedPropsMemo: () => false});
|
||||
|
||||
const {Animated} = importModules();
|
||||
|
||||
const opacity = new Animated.Value(0);
|
||||
opacity.__makeNative();
|
||||
|
||||
@@ -1301,6 +1429,8 @@ describe('Native Animated', () => {
|
||||
});
|
||||
|
||||
it('connects the native view on mount and disconnects on unmount', async () => {
|
||||
const {Animated} = importModules();
|
||||
|
||||
const opacity = new Animated.Value(0, {useNativeDriver: true});
|
||||
|
||||
const root = await create(<Animated.View style={{opacity}} />);
|
||||
@@ -1322,6 +1452,8 @@ describe('Native Animated', () => {
|
||||
});
|
||||
|
||||
it('reconnects the native view when the component is remounted', async () => {
|
||||
const {Animated} = importModules();
|
||||
|
||||
const opacity = new Animated.Value(0, {useNativeDriver: true});
|
||||
const ref = createRef();
|
||||
await create(<Animated.View ref={ref} style={{opacity}} />);
|
||||
|
||||
+369
@@ -0,0 +1,369 @@
|
||||
/**
|
||||
* 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
|
||||
* @oncall react_native
|
||||
*/
|
||||
|
||||
import {AnimatedEvent} from '../../../../Libraries/Animated/AnimatedEvent';
|
||||
import AnimatedValue from '../../../../Libraries/Animated/nodes/AnimatedValue';
|
||||
import {
|
||||
createCompositeKeyForProps,
|
||||
areCompositeKeysEqual,
|
||||
} from '../useAnimatedPropsMemo';
|
||||
|
||||
describe('createCompositeKeyForProps', () => {
|
||||
describe('with allowlist', () => {
|
||||
it('excludes non-array and non-object allowlisted props', () => {
|
||||
const props = {string: 'abc', number: 123, boolean: true, function() {}};
|
||||
const allowlist = {
|
||||
string: true,
|
||||
number: true,
|
||||
boolean: true,
|
||||
function: true,
|
||||
};
|
||||
const compositeKey = createCompositeKeyForProps(props, allowlist);
|
||||
|
||||
expect(compositeKey).toEqual(null);
|
||||
});
|
||||
|
||||
it('does not search non-allowlisted props', () => {
|
||||
const getter = jest.fn().mockReturnValue({});
|
||||
const props = {
|
||||
object: {
|
||||
// $FlowExpectedError[unsafe-getters-setters]
|
||||
get property() {
|
||||
return getter();
|
||||
},
|
||||
},
|
||||
};
|
||||
const allowlist = {};
|
||||
const compositeKey = createCompositeKeyForProps(props, allowlist);
|
||||
|
||||
expect(compositeKey).toEqual(null);
|
||||
expect(getter).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('includes allowlisted `AnimatedEvent` props at first depth', () => {
|
||||
const props = {
|
||||
foo: new AnimatedEvent([], {useNativeDriver: true}),
|
||||
bar: new AnimatedEvent([], {useNativeDriver: true}),
|
||||
};
|
||||
const allowlist = {
|
||||
bar: true,
|
||||
};
|
||||
const compositeKey = createCompositeKeyForProps(props, allowlist);
|
||||
|
||||
expect(compositeKey).toEqual({bar: props.bar});
|
||||
expect(compositeKey?.bar).toBe(props.bar);
|
||||
});
|
||||
|
||||
it('excludes allowlisted `AnimatedEvent` props in the `style` prop', () => {
|
||||
const props = {
|
||||
style: {
|
||||
// This is invalid usage, but including for testing.
|
||||
baz: new AnimatedEvent([], {useNativeDriver: true}),
|
||||
},
|
||||
};
|
||||
const allowlist = {
|
||||
style: {
|
||||
baz: true,
|
||||
},
|
||||
};
|
||||
const compositeKey = createCompositeKeyForProps(props, allowlist);
|
||||
|
||||
expect(compositeKey).toEqual(null);
|
||||
});
|
||||
|
||||
it('includes allowlisted `AnimatedNode` props', () => {
|
||||
const props = {
|
||||
foo: new AnimatedValue(1),
|
||||
bar: new AnimatedValue(1),
|
||||
};
|
||||
const allowlist = {
|
||||
bar: true,
|
||||
};
|
||||
const compositeKey = createCompositeKeyForProps(props, allowlist);
|
||||
|
||||
expect(compositeKey).toEqual({bar: props.bar});
|
||||
expect(compositeKey?.bar).toBe(props.bar);
|
||||
});
|
||||
|
||||
it('excludes non-allowlisted `style` props', () => {
|
||||
const props = {
|
||||
style: {opacity: new AnimatedValue(1)},
|
||||
};
|
||||
const allowlist = {};
|
||||
const compositeKey = createCompositeKeyForProps(props, allowlist);
|
||||
|
||||
expect(compositeKey).toEqual(null);
|
||||
});
|
||||
|
||||
it('searches the `style` prop for allowlisted `AnimatedNode` instances', () => {
|
||||
const opacity = new AnimatedValue(1);
|
||||
const rotateY = new AnimatedValue(1);
|
||||
const props = {
|
||||
style: {opacity, transform: [{rotateX: 1}, {rotateY}, {rotateZ: 1}]},
|
||||
};
|
||||
const allowlist = {style: {transform: true}};
|
||||
const compositeKey = createCompositeKeyForProps(props, allowlist);
|
||||
|
||||
expect(compositeKey).toEqual({
|
||||
style: {
|
||||
transform: [null, {rotateY}, null],
|
||||
},
|
||||
});
|
||||
// $FlowIgnore[prop-missing]
|
||||
expect(compositeKey?.style?.transform?.[1]?.rotateY).toBe(rotateY);
|
||||
});
|
||||
|
||||
it('flattens the `style` prop before searching it', () => {
|
||||
const opacityA = new AnimatedValue(1);
|
||||
const opacityB = new AnimatedValue(1);
|
||||
const props = {
|
||||
style: [{opacity: opacityA}, {opacity: opacityB}],
|
||||
};
|
||||
const allowlist = {style: {opacity: true}};
|
||||
const compositeKey = createCompositeKeyForProps(props, allowlist);
|
||||
|
||||
expect(compositeKey).toEqual({style: {opacity: opacityB}});
|
||||
expect(compositeKey?.style?.opacity).toBe(opacityB);
|
||||
});
|
||||
});
|
||||
|
||||
describe('without allowlist', () => {
|
||||
it('excludes non-array and non-object props', () => {
|
||||
const props = {string: 'abc', number: 123, boolean: true, function() {}};
|
||||
const compositeKey = createCompositeKeyForProps(props);
|
||||
|
||||
expect(compositeKey).toEqual(null);
|
||||
});
|
||||
|
||||
it('includes array props without searching them', () => {
|
||||
const props = {array: [{letter: 'a'}, {letter: 'b'}, {letter: 'c'}]};
|
||||
const compositeKey = createCompositeKeyForProps(props);
|
||||
|
||||
expect(compositeKey).toEqual({array: props.array});
|
||||
expect(compositeKey?.array).toBe(props.array);
|
||||
});
|
||||
|
||||
it('includes object props without searching them', () => {
|
||||
const props = {object: {foo: [1], bar: [2], baz: [3]}};
|
||||
const compositeKey = createCompositeKeyForProps(props);
|
||||
|
||||
expect(compositeKey).toEqual({object: props.object});
|
||||
expect(compositeKey?.object).toBe(props.object);
|
||||
});
|
||||
|
||||
it('includes `AnimatedEvent` props at first depth', () => {
|
||||
const props = {
|
||||
foo: new AnimatedEvent([], {useNativeDriver: true}),
|
||||
object: {
|
||||
bar: new AnimatedEvent([], {useNativeDriver: true}),
|
||||
},
|
||||
};
|
||||
const compositeKey = createCompositeKeyForProps(props);
|
||||
|
||||
expect(compositeKey).toEqual({
|
||||
foo: props.foo,
|
||||
object: props.object,
|
||||
});
|
||||
expect(compositeKey?.foo).toBe(props.foo);
|
||||
expect(compositeKey?.object).toBe(props.object);
|
||||
});
|
||||
|
||||
it('excludes `AnimatedEvent` props in the `style` prop', () => {
|
||||
const props = {
|
||||
style: {
|
||||
// This is invalid usage, but including for testing.
|
||||
baz: new AnimatedEvent([], {useNativeDriver: true}),
|
||||
},
|
||||
};
|
||||
const compositeKey = createCompositeKeyForProps(props);
|
||||
|
||||
expect(compositeKey).toEqual(null);
|
||||
});
|
||||
|
||||
it('includes `AnimatedNode` props', () => {
|
||||
const foo = new AnimatedValue(1);
|
||||
const bar = new AnimatedValue(1);
|
||||
const props = {foo, bar};
|
||||
const compositeKey = createCompositeKeyForProps(props);
|
||||
|
||||
expect(compositeKey).toEqual({foo, bar});
|
||||
expect(compositeKey?.foo).toBe(foo);
|
||||
expect(compositeKey?.bar).toBe(bar);
|
||||
});
|
||||
|
||||
it('searches the `style` prop for `AnimatedNode` instances', () => {
|
||||
const opacity = new AnimatedValue(1);
|
||||
const rotateY = new AnimatedValue(1);
|
||||
const props = {
|
||||
style: {opacity, transform: [{rotateX: 1}, {rotateY}, {rotateZ: 1}]},
|
||||
};
|
||||
const compositeKey = createCompositeKeyForProps(props);
|
||||
|
||||
expect(compositeKey).toEqual({
|
||||
style: {
|
||||
opacity,
|
||||
transform: [null, {rotateY}, null],
|
||||
},
|
||||
});
|
||||
expect(compositeKey?.style?.opacity).toBe(opacity);
|
||||
// $FlowIgnore[prop-missing]
|
||||
expect(compositeKey?.style?.transform?.[1]?.rotateY).toBe(rotateY);
|
||||
});
|
||||
|
||||
it('flattens the `style` prop before searching it', () => {
|
||||
const opacityA = new AnimatedValue(1);
|
||||
const opacityB = new AnimatedValue(1);
|
||||
const props = {
|
||||
style: [{opacity: opacityA}, {opacity: opacityB}],
|
||||
};
|
||||
const compositeKey = createCompositeKeyForProps(props);
|
||||
|
||||
expect(compositeKey).toEqual({
|
||||
style: {opacity: opacityB},
|
||||
});
|
||||
expect(compositeKey?.style?.opacity).toBe(opacityB);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('areCompositeKeysEqual', () => {
|
||||
it('compares identical keys without traversal', () => {
|
||||
const getter = jest.fn().mockReturnValue({});
|
||||
const compositeKey = {
|
||||
object: {
|
||||
// $FlowExpectedError[unsafe-getters-setters]
|
||||
get property() {
|
||||
return getter();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(areCompositeKeysEqual(compositeKey, compositeKey, null)).toBe(true);
|
||||
expect(getter).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('compares null keys', () => {
|
||||
const compositeKey = {foo: new AnimatedValue(1)};
|
||||
|
||||
expect(areCompositeKeysEqual(null, null, null)).toBe(true);
|
||||
expect(areCompositeKeysEqual(null, compositeKey, null)).toBe(false);
|
||||
expect(areCompositeKeysEqual(compositeKey, null, null)).toBe(false);
|
||||
});
|
||||
|
||||
it('compares keys with different lengths', () => {
|
||||
const compositeKeyA = {foo: new AnimatedValue(1)};
|
||||
const compositeKeyB = {
|
||||
foo: new AnimatedValue(1),
|
||||
bar: new AnimatedValue(1),
|
||||
};
|
||||
|
||||
expect(areCompositeKeysEqual(compositeKeyA, compositeKeyB, null)).toBe(
|
||||
false,
|
||||
);
|
||||
expect(areCompositeKeysEqual(compositeKeyB, compositeKeyA, null)).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('compares keys with `AnimatedNode` instances', () => {
|
||||
const foo = new AnimatedValue(1);
|
||||
const bar = new AnimatedValue(1);
|
||||
|
||||
expect(areCompositeKeysEqual({foo, bar}, {foo, bar}, null)).toBe(true);
|
||||
expect(areCompositeKeysEqual({foo}, {foo: bar}, null)).toBe(false);
|
||||
});
|
||||
|
||||
it('compares keys with `AnimatedEvent` instances', () => {
|
||||
const foo = new AnimatedEvent([], {useNativeDriver: true});
|
||||
const bar = new AnimatedEvent([], {useNativeDriver: true});
|
||||
|
||||
expect(areCompositeKeysEqual({foo, bar}, {foo, bar}, null)).toBe(true);
|
||||
expect(areCompositeKeysEqual({foo}, {foo: bar}, null)).toBe(false);
|
||||
});
|
||||
|
||||
it('compares keys with `style` props and identical `AnimatedNode`', () => {
|
||||
const opacity = new AnimatedValue(1);
|
||||
const rotateY = new AnimatedValue(1);
|
||||
const compositeKeyA = {
|
||||
style: {
|
||||
opacity,
|
||||
transform: [null, {rotateY}, null],
|
||||
},
|
||||
};
|
||||
const compositeKeyB = {
|
||||
style: {
|
||||
opacity,
|
||||
transform: [null, {rotateY}, null],
|
||||
},
|
||||
};
|
||||
|
||||
expect(areCompositeKeysEqual(compositeKeyA, compositeKeyB, null)).toBe(
|
||||
true,
|
||||
);
|
||||
expect(areCompositeKeysEqual(compositeKeyB, compositeKeyA, null)).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('compares keys with `style` props and different `AnimatedNode`', () => {
|
||||
const opacity = new AnimatedValue(1);
|
||||
const compositeKeyA = {
|
||||
style: {
|
||||
opacity,
|
||||
transform: [null, {rotateY: new AnimatedValue(1)}, null],
|
||||
},
|
||||
};
|
||||
const compositeKeyB = {
|
||||
style: {
|
||||
opacity,
|
||||
transform: [null, {rotateY: new AnimatedValue(1)}, null],
|
||||
},
|
||||
};
|
||||
|
||||
expect(areCompositeKeysEqual(compositeKeyA, compositeKeyB, null)).toBe(
|
||||
false,
|
||||
);
|
||||
expect(areCompositeKeysEqual(compositeKeyB, compositeKeyA, null)).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('compares keys with arrays and objects', () => {
|
||||
const bar = new AnimatedValue(1);
|
||||
|
||||
// If not in the allowlist, must be `===`.
|
||||
expect(areCompositeKeysEqual({foo: [bar]}, {foo: [bar]}, null)).toBe(false);
|
||||
expect(areCompositeKeysEqual({foo: {bar}}, {foo: {bar}}, null)).toBe(false);
|
||||
|
||||
// If in the allowlist, arrays and objects are traversed.
|
||||
expect(areCompositeKeysEqual({foo: [bar]}, {foo: [bar]}, {foo: true})).toBe(
|
||||
true,
|
||||
);
|
||||
expect(areCompositeKeysEqual({foo: {bar}}, {foo: {bar}}, {foo: true})).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('compares arrays with identical `AnimatedNode` at different indices', () => {
|
||||
const bar = new AnimatedValue(1);
|
||||
|
||||
expect(
|
||||
areCompositeKeysEqual({foo: [bar, null]}, {foo: [null, bar]}, null),
|
||||
).toBe(false);
|
||||
expect(
|
||||
areCompositeKeysEqual(
|
||||
{foo: [bar, null]},
|
||||
{foo: [null, bar]},
|
||||
{foo: true},
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,349 @@
|
||||
/**
|
||||
* 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
|
||||
* @oncall react_native
|
||||
*/
|
||||
|
||||
import type AnimatedProps from '../../../Libraries/Animated/nodes/AnimatedProps';
|
||||
import type {AnimatedPropsAllowlist} from '../../../Libraries/Animated/nodes/AnimatedProps';
|
||||
import type {AnimatedStyleAllowlist} from '../../../Libraries/Animated/nodes/AnimatedStyle';
|
||||
|
||||
import {AnimatedEvent} from '../../../Libraries/Animated/AnimatedEvent';
|
||||
import AnimatedNode from '../../../Libraries/Animated/nodes/AnimatedNode';
|
||||
import {isPlainObject} from '../../../Libraries/Animated/nodes/AnimatedObject';
|
||||
import flattenStyle from '../../../Libraries/StyleSheet/flattenStyle';
|
||||
|
||||
import nullthrows from 'nullthrows';
|
||||
import {useMemo, useState} from 'react';
|
||||
|
||||
type CompositeKey = {
|
||||
style?: {[string]: CompositeKeyComponent},
|
||||
[string]:
|
||||
| CompositeKeyComponent
|
||||
| AnimatedEvent
|
||||
| $ReadOnlyArray<mixed>
|
||||
| $ReadOnly<{[string]: mixed}>,
|
||||
};
|
||||
|
||||
type CompositeKeyComponent =
|
||||
| AnimatedNode
|
||||
| Array<CompositeKeyComponent | null>
|
||||
| {[string]: CompositeKeyComponent};
|
||||
|
||||
type $ReadOnlyCompositeKey = $ReadOnly<{
|
||||
style?: $ReadOnly<{[string]: CompositeKeyComponent}>,
|
||||
[string]:
|
||||
| $ReadOnlyCompositeKeyComponent
|
||||
| AnimatedEvent
|
||||
| $ReadOnlyArray<mixed>
|
||||
| $ReadOnly<{[string]: mixed}>,
|
||||
}>;
|
||||
|
||||
type $ReadOnlyCompositeKeyComponent =
|
||||
| AnimatedNode
|
||||
| $ReadOnlyArray<$ReadOnlyCompositeKeyComponent | null>
|
||||
| $ReadOnly<{[string]: $ReadOnlyCompositeKeyComponent}>;
|
||||
|
||||
/**
|
||||
* A hook that returns an `AnimatedProps` object that is memoized based on the
|
||||
* subset of `props` that are instances of `AnimatedNode` or `AnimatedEvent`.
|
||||
*/
|
||||
export function useAnimatedPropsMemo(
|
||||
create: () => AnimatedProps,
|
||||
// TODO: Make this two separate arguments after the experiment is over. This
|
||||
// is only an array-like structure to make it easier to experiment with this
|
||||
// and `useMemo`.
|
||||
[allowlist, props]: [?AnimatedPropsAllowlist, {[string]: mixed}],
|
||||
): AnimatedProps {
|
||||
const compositeKey = useMemo(
|
||||
() => createCompositeKeyForProps(props, allowlist),
|
||||
[allowlist, props],
|
||||
);
|
||||
|
||||
const [state, setState] = useState<{
|
||||
allowlist: ?AnimatedPropsAllowlist,
|
||||
compositeKey: $ReadOnlyCompositeKey | null,
|
||||
value: AnimatedProps,
|
||||
}>(() => ({
|
||||
allowlist,
|
||||
compositeKey,
|
||||
value: create(),
|
||||
}));
|
||||
|
||||
if (
|
||||
state.allowlist !== allowlist ||
|
||||
!areCompositeKeysEqual(state.compositeKey, compositeKey)
|
||||
) {
|
||||
setState({
|
||||
allowlist,
|
||||
compositeKey,
|
||||
value: create(),
|
||||
});
|
||||
}
|
||||
return state.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new composite key for a `props` object that can be used to detect
|
||||
* whether a new `AnimatedProps` instance must be created.
|
||||
*
|
||||
* - With an allowlist, those props are searched for `AnimatedNode` instances.
|
||||
* - Without an allowlist, `style` is searched for `AnimatedNode` instances,
|
||||
* but all other objects and arrays are included (not searched). We do not
|
||||
* search objects and arrays without an allowlist in case they are very large
|
||||
* data structures. We safely traverse `style` becuase it is bounded.
|
||||
*
|
||||
* Any `AnimatedEvent` instances at the first depth are always included.
|
||||
*
|
||||
* If `props` contains no `AnimatedNode` or `AnimatedEvent` instances, this
|
||||
* returns null.
|
||||
*/
|
||||
export function createCompositeKeyForProps(
|
||||
props: $ReadOnly<{[string]: mixed}>,
|
||||
allowlist: ?AnimatedPropsAllowlist,
|
||||
): $ReadOnlyCompositeKey | null {
|
||||
let compositeKey: CompositeKey | null = null;
|
||||
|
||||
const keys = Object.keys(props);
|
||||
for (let ii = 0, length = keys.length; ii < length; ii++) {
|
||||
const key = keys[ii];
|
||||
const value = props[key];
|
||||
|
||||
if (allowlist == null || Object.hasOwn(allowlist, key)) {
|
||||
let compositeKeyComponent;
|
||||
if (key === 'style') {
|
||||
// $FlowFixMe[incompatible-call] - `style` is a valid argument.
|
||||
// $FlowFixMe[incompatible-type] - `flattenStyle` returns an object.
|
||||
const flatStyle: ?{[string]: mixed} = flattenStyle(value);
|
||||
if (flatStyle != null) {
|
||||
compositeKeyComponent = createCompositeKeyForObject(
|
||||
flatStyle,
|
||||
allowlist?.style,
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
value instanceof AnimatedNode ||
|
||||
value instanceof AnimatedEvent
|
||||
) {
|
||||
compositeKeyComponent = value;
|
||||
} else if (Array.isArray(value)) {
|
||||
compositeKeyComponent =
|
||||
allowlist == null ? value : createCompositeKeyForArray(value);
|
||||
} else if (isPlainObject(value)) {
|
||||
compositeKeyComponent =
|
||||
allowlist == null ? value : createCompositeKeyForObject(value);
|
||||
}
|
||||
if (compositeKeyComponent != null) {
|
||||
if (compositeKey == null) {
|
||||
compositeKey = {} as CompositeKey;
|
||||
}
|
||||
compositeKey[key] = compositeKeyComponent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return compositeKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new composite key for an array that retains all values that are or
|
||||
* contain `AnimatedNode` instances, and `null` for the rest.
|
||||
*
|
||||
* If `array` contains no `AnimatedNode` instances, this returns null.
|
||||
*/
|
||||
function createCompositeKeyForArray(
|
||||
array: $ReadOnlyArray<mixed>,
|
||||
): $ReadOnlyArray<$ReadOnlyCompositeKeyComponent | null> | null {
|
||||
let compositeKey: Array<$ReadOnlyCompositeKeyComponent | null> | null = null;
|
||||
|
||||
for (let ii = 0, length = array.length; ii < length; ii++) {
|
||||
const value = array[ii];
|
||||
|
||||
let compositeKeyComponent;
|
||||
if (value instanceof AnimatedNode) {
|
||||
compositeKeyComponent = value;
|
||||
} else if (Array.isArray(value)) {
|
||||
compositeKeyComponent = createCompositeKeyForArray(value);
|
||||
} else if (isPlainObject(value)) {
|
||||
compositeKeyComponent = createCompositeKeyForObject(value);
|
||||
}
|
||||
if (compositeKeyComponent != null) {
|
||||
if (compositeKey == null) {
|
||||
compositeKey = new Array<$ReadOnlyCompositeKeyComponent | null>(
|
||||
array.length,
|
||||
).fill(null);
|
||||
}
|
||||
compositeKey[ii] = compositeKeyComponent;
|
||||
}
|
||||
}
|
||||
|
||||
return compositeKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new composite key for an object that retains only properties that
|
||||
* are or contain `AnimatedNode` instances.
|
||||
*
|
||||
* When used to create composite keys for `style` props:
|
||||
*
|
||||
* - With an allowlist, those properties are searched.
|
||||
* - Without an allowlist, every property is searched.
|
||||
*
|
||||
* If `object` contains no `AnimatedNode` instances, this returns null.
|
||||
*/
|
||||
function createCompositeKeyForObject(
|
||||
object: $ReadOnly<{[string]: mixed}>,
|
||||
allowlist?: ?AnimatedStyleAllowlist,
|
||||
): $ReadOnly<{[string]: $ReadOnlyCompositeKeyComponent}> | null {
|
||||
let compositeKey: {[string]: $ReadOnlyCompositeKeyComponent} | null = null;
|
||||
|
||||
const keys = Object.keys(object);
|
||||
for (let ii = 0, length = keys.length; ii < length; ii++) {
|
||||
const key = keys[ii];
|
||||
|
||||
if (allowlist == null || Object.hasOwn(allowlist, key)) {
|
||||
const value = object[key];
|
||||
|
||||
let compositeKeyComponent;
|
||||
if (value instanceof AnimatedNode) {
|
||||
compositeKeyComponent = value;
|
||||
} else if (Array.isArray(value)) {
|
||||
compositeKeyComponent = createCompositeKeyForArray(value);
|
||||
} else if (isPlainObject(value)) {
|
||||
compositeKeyComponent = createCompositeKeyForObject(value);
|
||||
}
|
||||
if (compositeKeyComponent != null) {
|
||||
if (compositeKey == null) {
|
||||
compositeKey = {} as {[string]: $ReadOnlyCompositeKeyComponent};
|
||||
}
|
||||
compositeKey[key] = compositeKeyComponent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return compositeKey;
|
||||
}
|
||||
|
||||
export function areCompositeKeysEqual(
|
||||
maybePrev: $ReadOnlyCompositeKey | null,
|
||||
maybeNext: $ReadOnlyCompositeKey | null,
|
||||
allowlist: ?AnimatedPropsAllowlist,
|
||||
): boolean {
|
||||
if (maybePrev === maybeNext) {
|
||||
return true;
|
||||
}
|
||||
if (maybePrev === null || maybeNext === null) {
|
||||
return false;
|
||||
}
|
||||
// Help Flow retain the type refinements of these.
|
||||
const prev = maybePrev;
|
||||
const next = maybeNext;
|
||||
|
||||
const keys = Object.keys(prev);
|
||||
const length = keys.length;
|
||||
if (length !== Object.keys(next).length) {
|
||||
return false;
|
||||
}
|
||||
for (let ii = 0; ii < length; ii++) {
|
||||
const key = keys[ii];
|
||||
if (!Object.hasOwn(next, key)) {
|
||||
return false;
|
||||
}
|
||||
const prevComponent = prev[key];
|
||||
const nextComponent = next[key];
|
||||
|
||||
if (key === 'style') {
|
||||
// We know style components are objects with non-mixed values.
|
||||
if (
|
||||
!areCompositeKeyComponentsEqual(
|
||||
// $FlowIgnore[incompatible-cast]
|
||||
prevComponent as $ReadOnlyCompositeKeyComponent,
|
||||
// $FlowIgnore[incompatible-cast]
|
||||
nextComponent as $ReadOnlyCompositeKeyComponent,
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
} else if (
|
||||
prevComponent instanceof AnimatedNode ||
|
||||
prevComponent instanceof AnimatedEvent
|
||||
) {
|
||||
if (prevComponent !== nextComponent) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// When `allowlist` is null, the components must be the same. Otherwise,
|
||||
// we created the components using deep traversal, so deep compare them.
|
||||
if (allowlist == null) {
|
||||
if (prevComponent !== nextComponent) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
!areCompositeKeyComponentsEqual(
|
||||
// $FlowIgnore[incompatible-cast]
|
||||
prevComponent as $ReadOnlyCompositeKeyComponent,
|
||||
// $FlowIgnore[incompatible-cast]
|
||||
nextComponent as $ReadOnlyCompositeKeyComponent,
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function areCompositeKeyComponentsEqual(
|
||||
prev: $ReadOnlyCompositeKeyComponent | null,
|
||||
next: $ReadOnlyCompositeKeyComponent | null,
|
||||
): boolean {
|
||||
if (prev === next) {
|
||||
return true;
|
||||
}
|
||||
if (prev instanceof AnimatedNode) {
|
||||
return prev === next;
|
||||
}
|
||||
if (Array.isArray(prev)) {
|
||||
if (!Array.isArray(next)) {
|
||||
return false;
|
||||
}
|
||||
const length = prev.length;
|
||||
if (length !== next.length) {
|
||||
return false;
|
||||
}
|
||||
for (let ii = 0; ii < length; ii++) {
|
||||
if (!areCompositeKeyComponentsEqual(prev[ii], next[ii])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (isPlainObject(prev)) {
|
||||
if (!isPlainObject(next)) {
|
||||
return false;
|
||||
}
|
||||
const keys = Object.keys(prev);
|
||||
const length = keys.length;
|
||||
if (length !== Object.keys(next).length) {
|
||||
return false;
|
||||
}
|
||||
for (let ii = 0; ii < length; ii++) {
|
||||
const key = keys[ii];
|
||||
if (
|
||||
!Object.hasOwn(nullthrows(next), key) ||
|
||||
!areCompositeKeyComponentsEqual(prev[key], next[key])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @generated SignedSource<<20de2dddd88f49d4f3a6491ca5c2fe6f>>
|
||||
* @generated SignedSource<<d6b7a8d93e08da169a3e6d60de9e3155>>
|
||||
* @flow strict-local
|
||||
*/
|
||||
|
||||
@@ -31,6 +31,7 @@ export type ReactNativeFeatureFlagsJsOnly = {
|
||||
animatedShouldUseSingleOp: Getter<boolean>,
|
||||
enableAccessToHostTreeInFabric: Getter<boolean>,
|
||||
enableAnimatedAllowlist: Getter<boolean>,
|
||||
enableAnimatedPropsMemo: Getter<boolean>,
|
||||
enableOptimisedVirtualizedCells: Getter<boolean>,
|
||||
isLayoutAnimationEnabled: Getter<boolean>,
|
||||
shouldSkipStateUpdatesForLoopingAnimations: Getter<boolean>,
|
||||
@@ -122,6 +123,11 @@ export const enableAccessToHostTreeInFabric: Getter<boolean> = createJavaScriptF
|
||||
*/
|
||||
export const enableAnimatedAllowlist: Getter<boolean> = createJavaScriptFlagGetter('enableAnimatedAllowlist', false);
|
||||
|
||||
/**
|
||||
* Enables Animated to analyze props to minimize invalidating `AnimatedProps`.
|
||||
*/
|
||||
export const enableAnimatedPropsMemo: Getter<boolean> = createJavaScriptFlagGetter('enableAnimatedPropsMemo', false);
|
||||
|
||||
/**
|
||||
* Removing unnecessary rerenders Virtualized cells after any rerenders of Virualized list. Works with strict=true option
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user