Skip to content

Commit

Permalink
Animated: Create Experimental Props Allowlist (#46374)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: #46374

Creates an experimental `allowlist` parameter that can be supplied to a new `unstable_createAnimatedComponentWithAllowlist` method for use in new experiments to evaluate the impact on product performance and developer experience.

When it is provided, only the props and styles in `allowlist` will be inspected for `AnimatedNode` values. The hypothesis for this change is that restricting the search space for `AnimatedProps` to an allowlist will significantly reduce the runtime overhead of using `Animated` components.

Changelog:
[General][Added] - Created an experimental (unstable) method for allowlisting props when using `Animated`

Reviewed By: javache

Differential Revision: D62117424

fbshipit-source-id: bdd656be1fdc7454360035627644606cb00d33c0
  • Loading branch information
yungsters authored and facebook-github-bot committed Sep 10, 2024
1 parent 5dbd9fc commit f3f652d
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
* @format
*/

import type {AnimatedPropsAllowlist} from './nodes/AnimatedProps';

import composeStyles from '../../src/private/styles/composeStyles';
import View from '../Components/View/View';
import useMergeRefs from '../Utilities/useMergeRefs';
Expand All @@ -32,12 +34,23 @@ export type AnimatedComponentType<

export default function createAnimatedComponent<TProps: {...}, TInstance>(
Component: React.AbstractComponent<TProps, TInstance>,
): AnimatedComponentType<TProps, TInstance> {
return unstable_createAnimatedComponentWithAllowlist(Component, null);
}

export function unstable_createAnimatedComponentWithAllowlist<
TProps: {...},
TInstance,
>(
Component: React.AbstractComponent<TProps, TInstance>,
allowlist: ?AnimatedPropsAllowlist,
): AnimatedComponentType<TProps, TInstance> {
const AnimatedComponent = React.forwardRef<AnimatedProps<TProps>, TInstance>(
(props, forwardedRef) => {
const [reducedProps, callbackRef] = useAnimatedProps<TProps, TInstance>(
// $FlowFixMe[incompatible-call]
props,
allowlist,
);
const ref = useMergeRefs<TInstance>(callbackRef, forwardedRef);

Expand Down
61 changes: 44 additions & 17 deletions packages/react-native/Libraries/Animated/nodes/AnimatedProps.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*/

import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type {AnimatedStyleAllowlist} from './AnimatedStyle';

import {findNodeHandle} from '../../ReactNative/RendererProxy';
import {AnimatedEvent} from '../AnimatedEvent';
Expand All @@ -18,9 +19,15 @@ import AnimatedObject from './AnimatedObject';
import AnimatedStyle from './AnimatedStyle';
import invariant from 'invariant';

function createAnimatedProps(inputProps: {
[string]: mixed,
}): [$ReadOnlyArray<string>, $ReadOnlyArray<AnimatedNode>, {[string]: mixed}] {
export type AnimatedPropsAllowlist = $ReadOnly<{
style?: ?AnimatedStyleAllowlist,
[string]: true,
}>;

function createAnimatedProps(
inputProps: {[string]: mixed},
allowlist: ?AnimatedPropsAllowlist,
): [$ReadOnlyArray<string>, $ReadOnlyArray<AnimatedNode>, {[string]: mixed}] {
const nodeKeys: Array<string> = [];
const nodes: Array<AnimatedNode> = [];
const props: {[string]: mixed} = {};
Expand All @@ -30,20 +37,36 @@ function createAnimatedProps(inputProps: {
const key = keys[ii];
const value = inputProps[key];

let node;
if (key === 'style') {
node = AnimatedStyle.from(value);
} else if (value instanceof AnimatedNode) {
node = value;
if (allowlist == null || Object.hasOwn(allowlist, key)) {
let node;
if (key === 'style') {
node = AnimatedStyle.from(value, allowlist?.style);
} else if (value instanceof AnimatedNode) {
node = value;
} else {
node = AnimatedObject.from(value);
}
if (node == null) {
props[key] = value;
} else {
nodeKeys.push(key);
nodes.push(node);
props[key] = node;
}
} else {
node = AnimatedObject.from(value);
}
if (node == null) {
if (__DEV__) {
// WARNING: This is a potentially expensive check that we should only
// do in development. Without this check in development, it might be
// difficult to identify which props need to be allowlisted.
if (AnimatedObject.from(inputProps[key]) != null) {
console.error(
`AnimatedProps: ${key} is not allowlisted for animation, but it ` +
'contains AnimatedNode values; props allowing animation: ',
allowlist,
);
}
}
props[key] = value;
} else {
nodeKeys.push(key);
nodes.push(node);
props[key] = node;
}
}

Expand All @@ -57,9 +80,13 @@ export default class AnimatedProps extends AnimatedNode {
#nodes: $ReadOnlyArray<AnimatedNode>;
#props: {[string]: mixed};

constructor(inputProps: {[string]: mixed}, callback: () => void) {
constructor(
inputProps: {[string]: mixed},
callback: () => void,
allowlist?: ?AnimatedPropsAllowlist,
) {
super();
const [nodeKeys, nodes, props] = createAnimatedProps(inputProps);
const [nodeKeys, nodes, props] = createAnimatedProps(inputProps, allowlist);
this.#nodeKeys = nodeKeys;
this.#nodes = nodes;
this.#props = props;
Expand Down
57 changes: 41 additions & 16 deletions packages/react-native/Libraries/Animated/nodes/AnimatedStyle.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ import AnimatedObject from './AnimatedObject';
import AnimatedTransform from './AnimatedTransform';
import AnimatedWithChildren from './AnimatedWithChildren';

export type AnimatedStyleAllowlist = $ReadOnly<{[string]: true}>;

function createAnimatedStyle(
inputStyle: {[string]: mixed},
allowlist: ?AnimatedStyleAllowlist,
keepUnanimatedValues: boolean,
): [$ReadOnlyArray<string>, $ReadOnlyArray<AnimatedNode>, {[string]: mixed}] {
const nodeKeys: Array<string> = [];
Expand All @@ -32,25 +35,43 @@ function createAnimatedStyle(
const key = keys[ii];
const value = inputStyle[key];

let node;
if (value != null && key === 'transform') {
node = ReactNativeFeatureFlags.shouldUseAnimatedObjectForTransform()
? AnimatedObject.from(value)
: // $FlowFixMe[incompatible-call] - `value` is mixed.
AnimatedTransform.from(value);
} else if (value instanceof AnimatedNode) {
node = value;
if (allowlist == null || Object.hasOwn(allowlist, key)) {
let node;
if (value != null && key === 'transform') {
node = ReactNativeFeatureFlags.shouldUseAnimatedObjectForTransform()
? AnimatedObject.from(value)
: // $FlowFixMe[incompatible-call] - `value` is mixed.
AnimatedTransform.from(value);
} else if (value instanceof AnimatedNode) {
node = value;
} else {
node = AnimatedObject.from(value);
}
if (node == null) {
if (keepUnanimatedValues) {
style[key] = value;
}
} else {
nodeKeys.push(key);
nodes.push(node);
style[key] = node;
}
} else {
node = AnimatedObject.from(value);
}
if (node == null) {
if (__DEV__) {
// WARNING: This is a potentially expensive check that we should only
// do in development. Without this check in development, it might be
// difficult to identify which styles need to be allowlisted.
if (AnimatedObject.from(inputStyle[key]) != null) {
console.error(
`AnimatedStyle: ${key} is not allowlisted for animation, but it ` +
'contains AnimatedNode values; styles allowing animation: ',
allowlist,
);
}
}
if (keepUnanimatedValues) {
style[key] = value;
}
} else {
nodeKeys.push(key);
nodes.push(node);
style[key] = node;
}
}

Expand All @@ -67,13 +88,17 @@ export default class AnimatedStyle extends AnimatedWithChildren {
* Creates an `AnimatedStyle` if `value` contains `AnimatedNode` instances.
* Otherwise, returns `null`.
*/
static from(inputStyle: any): ?AnimatedStyle {
static from(
inputStyle: any,
allowlist: ?AnimatedStyleAllowlist,
): ?AnimatedStyle {
const flatStyle = flattenStyle(inputStyle);
if (flatStyle == null) {
return null;
}
const [nodeKeys, nodes, style] = createAnimatedStyle(
flatStyle,
allowlist,
Platform.OS !== 'web',
);
if (nodes.length === 0) {
Expand Down
8 changes: 4 additions & 4 deletions packages/react-native/Libraries/Animated/useAnimatedProps.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
* @format
*/

'use strict';

import type {AnimatedPropsAllowlist} from './nodes/AnimatedProps';
import type {EventSubscription} from '../EventEmitter/NativeEventEmitter';

import * as ReactNativeFeatureFlags from '../../src/private/featureflags/ReactNativeFeatureFlags';
Expand Down Expand Up @@ -43,6 +42,7 @@ type AnimatedValueListeners = Array<{

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);
Expand All @@ -53,8 +53,8 @@ export default function useAnimatedProps<TProps: {...}, TInstance>(
// same name property name as styles, so we can probably continue doing that.
// The ordering of other props *should* not matter.
const node = useMemo(
() => new AnimatedProps(props, () => onUpdateRef.current?.()),
[props],
() => new AnimatedProps(props, () => onUpdateRef.current?.(), allowlist),
[allowlist, props],
);
const useNativePropsInFabric =
ReactNativeFeatureFlags.shouldUseSetNativePropsInFabric();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,13 @@ declare export default function createAnimatedComponent<
>(
Component: React.AbstractComponent<TProps, TInstance>
): AnimatedComponentType<TProps, TInstance>;
declare export function unstable_createAnimatedComponentWithAllowlist<
TProps: { ... },
TInstance,
>(
Component: React.AbstractComponent<TProps, TInstance>,
allowlist: ?AnimatedPropsAllowlist
): AnimatedComponentType<TProps, TInstance>;
"
`;

Expand Down Expand Up @@ -945,8 +952,16 @@ exports[`public API should not change unintentionally Libraries/Animated/nodes/A
`;

exports[`public API should not change unintentionally Libraries/Animated/nodes/AnimatedProps.js 1`] = `
"declare export default class AnimatedProps extends AnimatedNode {
constructor(inputProps: { [string]: mixed }, callback: () => void): void;
"export type AnimatedPropsAllowlist = $ReadOnly<{
style?: ?AnimatedStyleAllowlist,
[string]: true,
}>;
declare export default class AnimatedProps extends AnimatedNode {
constructor(
inputProps: { [string]: mixed },
callback: () => void,
allowlist?: ?AnimatedPropsAllowlist
): void;
__getValue(): Object;
__getAnimatedValue(): Object;
__attach(): void;
Expand All @@ -963,8 +978,12 @@ exports[`public API should not change unintentionally Libraries/Animated/nodes/A
`;

exports[`public API should not change unintentionally Libraries/Animated/nodes/AnimatedStyle.js 1`] = `
"declare export default class AnimatedStyle extends AnimatedWithChildren {
static from(inputStyle: any): ?AnimatedStyle;
"export type AnimatedStyleAllowlist = $ReadOnly<{ [string]: true }>;
declare export default class AnimatedStyle extends AnimatedWithChildren {
static from(
inputStyle: any,
allowlist: ?AnimatedStyleAllowlist
): ?AnimatedStyle;
constructor(
nodeKeys: $ReadOnlyArray<string>,
nodes: $ReadOnlyArray<AnimatedNode>,
Expand Down Expand Up @@ -1159,7 +1178,8 @@ exports[`public API should not change unintentionally Libraries/Animated/useAnim
};
type CallbackRef<T> = (T) => mixed;
declare export default function useAnimatedProps<TProps: { ... }, TInstance>(
props: TProps
props: TProps,
allowlist?: ?AnimatedPropsAllowlist
): [ReducedProps<TProps>, CallbackRef<TInstance | null>];
"
`;
Expand Down

0 comments on commit f3f652d

Please sign in to comment.