chore: update map

This commit is contained in:
2026-02-19 09:03:43 +01:00
parent 900c54c20b
commit 823ea7e739
64 changed files with 1839 additions and 3662 deletions
@@ -1,24 +1,17 @@
'use strict';
import type { StyleProps } from '../reanimated2';
import type { StyleProps } from '../commonTypes';
import { isSharedValue } from '../isSharedValue';
import { startMapper, stopMapper } from '../mappers';
import { updateProps } from '../updateProps';
import type { ViewDescriptorsSet } from '../ViewDescriptorsSet';
import { makeViewDescriptorsSet } from '../ViewDescriptorsSet';
import type {
IAnimatedComponentInternal,
AnimatedComponentProps,
AnimatedComponentType,
IInlinePropManager,
ViewInfo,
} from './commonTypes';
import { flattenArray } from './utils';
import { makeViewDescriptorsSet } from '../reanimated2/ViewDescriptorsSet';
import type {
ViewDescriptorsSet,
ViewRefSet,
} from '../reanimated2/ViewDescriptorsSet';
import { adaptViewConfig } from '../ConfigHelper';
import updateProps from '../reanimated2/UpdateProps';
import { stopMapper, startMapper } from '../reanimated2/mappers';
import { isSharedValue } from '../reanimated2/isSharedValue';
import { shouldBeUseWeb } from '../reanimated2/PlatformChecker';
const SHOULD_BE_USE_WEB = shouldBeUseWeb();
function isInlineStyleTransform(transform: unknown): boolean {
if (!Array.isArray(transform)) {
@@ -37,29 +30,30 @@ function inlinePropsHasChanged(
}
for (const key of Object.keys(styles1)) {
if (styles1[key] !== styles2[key]) return true;
if (styles1[key] !== styles2[key]) {
return true;
}
}
return false;
}
function getInlinePropsUpdate(inlineProps: Record<string, unknown>) {
function getInlinePropsUpdate(styleValue: StyleProps): unknown {
'worklet';
const update: Record<string, unknown> = {};
for (const [key, styleValue] of Object.entries(inlineProps)) {
if (isSharedValue(styleValue)) {
update[key] = styleValue.value;
} else if (Array.isArray(styleValue)) {
update[key] = styleValue.map((item) => {
return getInlinePropsUpdate(item);
});
} else if (typeof styleValue === 'object') {
update[key] = getInlinePropsUpdate(styleValue as Record<string, unknown>);
} else {
update[key] = styleValue;
}
if (isSharedValue(styleValue)) {
return styleValue.value;
}
return update;
if (Array.isArray(styleValue)) {
return styleValue.map(getInlinePropsUpdate);
}
if (styleValue && typeof styleValue === 'object') {
const update: Record<string, unknown> = {};
for (const [key, value] of Object.entries(styleValue)) {
update[key] = getInlinePropsUpdate(value);
}
return update;
}
return styleValue;
}
function extractSharedValuesMapFromProps(
@@ -77,14 +71,14 @@ function extractSharedValuesMapFromProps(
if (!style) {
return;
}
for (const [key, styleValue] of Object.entries(style)) {
for (const [styleKey, styleValue] of Object.entries(style)) {
if (isSharedValue(styleValue)) {
inlineProps[key] = styleValue;
inlineProps[styleKey] = styleValue;
} else if (
key === 'transform' &&
styleKey === 'transform' &&
isInlineStyleTransform(styleValue)
) {
inlineProps[key] = styleValue;
inlineProps[styleKey] = styleValue;
}
}
});
@@ -114,7 +108,7 @@ export function getInlineStyle(
isFirstRender: boolean
) {
if (isFirstRender) {
return getInlinePropsUpdate(style);
return getInlinePropsUpdate(style) as Record<string, unknown>;
}
const newStyle: StyleProps = {};
for (const [key, styleValue] of Object.entries(style)) {
@@ -134,8 +128,7 @@ export class InlinePropManager implements IInlinePropManager {
_inlineProps: StyleProps = {};
public attachInlineProps(
animatedComponent: React.Component<unknown, unknown> &
IAnimatedComponentInternal,
animatedComponent: AnimatedComponentType,
viewInfo: ViewInfo
) {
const newInlineProps: Record<string, unknown> =
@@ -146,31 +139,20 @@ export class InlinePropManager implements IInlinePropManager {
if (!this._inlinePropsViewDescriptors) {
this._inlinePropsViewDescriptors = makeViewDescriptorsSet();
const { viewTag, viewName, shadowNodeWrapper, viewConfig } = viewInfo;
if (Object.keys(newInlineProps).length && viewConfig) {
adaptViewConfig(viewConfig);
}
const { viewTag, shadowNodeWrapper } = viewInfo;
this._inlinePropsViewDescriptors.add({
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
tag: viewTag as number,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
name: viewName!,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
shadowNodeWrapper: shadowNodeWrapper!,
});
}
const shareableViewDescriptors =
this._inlinePropsViewDescriptors.shareableViewDescriptors;
const maybeViewRef = SHOULD_BE_USE_WEB
? ({ items: new Set([animatedComponent]) } as ViewRefSet<unknown>) // see makeViewsRefSet
: undefined;
const updaterFunction = () => {
'worklet';
const update = getInlinePropsUpdate(newInlineProps);
updateProps(shareableViewDescriptors, update, maybeViewRef);
updateProps(shareableViewDescriptors, update);
};
this._inlineProps = newInlineProps;
if (this._inlinePropsMapperId) {
@@ -1,124 +1,55 @@
'use strict';
import {
NativeEventEmitter,
NativeModules,
findNodeHandle,
} from 'react-native';
import { shouldBeUseWeb } from '../reanimated2/PlatformChecker';
import type { StyleProps } from '../reanimated2';
import { runOnJS, runOnUIImmediately } from '../reanimated2/threads';
import { runOnUI } from 'react-native-worklets';
import { SHOULD_BE_USE_WEB } from '../common';
import type {
AnimatedComponentProps,
AnimatedComponentType,
IAnimatedComponentInternal,
IJSPropsUpdater,
InitialComponentProps,
JSPropsOperation,
} from './commonTypes';
interface ListenerData {
viewTag: number;
props: StyleProps;
}
class JSPropsUpdaterNative implements IJSPropsUpdater {
private static _tagToComponentMapping = new Map<
number,
AnimatedComponentType
>();
const SHOULD_BE_USE_WEB = shouldBeUseWeb();
class JSPropsUpdaterPaper implements IJSPropsUpdater {
private static _tagToComponentMapping = new Map();
private _reanimatedEventEmitter: NativeEventEmitter;
constructor() {
this._reanimatedEventEmitter = new NativeEventEmitter(
NativeModules.ReanimatedModule
);
}
public addOnJSPropsChangeListener(
animatedComponent: React.Component<
AnimatedComponentProps<InitialComponentProps>
> &
IAnimatedComponentInternal
public registerComponent(
animatedComponent: AnimatedComponentType,
jsProps: string[]
) {
const viewTag = findNodeHandle(animatedComponent);
JSPropsUpdaterPaper._tagToComponentMapping.set(viewTag, animatedComponent);
if (JSPropsUpdaterPaper._tagToComponentMapping.size === 1) {
const listener = (data: ListenerData) => {
const component = JSPropsUpdaterPaper._tagToComponentMapping.get(
data.viewTag
);
component?._updateFromNative(data.props);
};
this._reanimatedEventEmitter.addListener(
'onReanimatedPropsChange',
listener
const viewTag = animatedComponent.getComponentViewTag();
JSPropsUpdaterNative._tagToComponentMapping.set(viewTag, animatedComponent);
runOnUI(() => {
global._tagToJSPropNamesMapping[viewTag] = Object.fromEntries(
jsProps.map((propName) => [propName, true])
);
}
})();
}
public removeOnJSPropsChangeListener(
animatedComponent: React.Component<
AnimatedComponentProps<InitialComponentProps>
> &
IAnimatedComponentInternal
) {
const viewTag = findNodeHandle(animatedComponent);
JSPropsUpdaterPaper._tagToComponentMapping.delete(viewTag);
if (JSPropsUpdaterPaper._tagToComponentMapping.size === 0) {
this._reanimatedEventEmitter.removeAllListeners(
'onReanimatedPropsChange'
);
}
}
}
public unregisterComponent(animatedComponent: AnimatedComponentType) {
const viewTag = animatedComponent.getComponentViewTag();
JSPropsUpdaterNative._tagToComponentMapping.delete(viewTag);
class JSPropsUpdaterFabric implements IJSPropsUpdater {
private static _tagToComponentMapping = new Map();
private static isInitialized = false;
constructor() {
if (!JSPropsUpdaterFabric.isInitialized) {
const updater = (viewTag: number, props: unknown) => {
const component =
JSPropsUpdaterFabric._tagToComponentMapping.get(viewTag);
component?._updateFromNative(props);
};
runOnUIImmediately(() => {
'worklet';
global.updateJSProps = (viewTag: number, props: unknown) => {
runOnJS(updater)(viewTag, props);
};
})();
JSPropsUpdaterFabric.isInitialized = true;
}
runOnUI(() => {
delete global._tagToJSPropNamesMapping[viewTag];
})();
}
public addOnJSPropsChangeListener(
animatedComponent: React.Component<
AnimatedComponentProps<InitialComponentProps>
> &
IAnimatedComponentInternal
) {
if (!JSPropsUpdaterFabric.isInitialized) {
return;
}
const viewTag = findNodeHandle(animatedComponent);
JSPropsUpdaterFabric._tagToComponentMapping.set(viewTag, animatedComponent);
}
public removeOnJSPropsChangeListener(
animatedComponent: React.Component<
AnimatedComponentProps<InitialComponentProps>
> &
IAnimatedComponentInternal
) {
if (!JSPropsUpdaterFabric.isInitialized) {
return;
}
const viewTag = findNodeHandle(animatedComponent);
JSPropsUpdaterFabric._tagToComponentMapping.delete(viewTag);
public updateProps(operations: JSPropsOperation[]) {
operations.forEach(({ tag, updates }) => {
const component = JSPropsUpdaterNative._tagToComponentMapping.get(tag);
component?.setNativeProps(updates);
});
}
}
class JSPropsUpdaterWeb implements IJSPropsUpdater {
public addOnJSPropsChangeListener(
public registerComponent(
_animatedComponent: React.Component<
AnimatedComponentProps<InitialComponentProps>
> &
@@ -127,7 +58,7 @@ class JSPropsUpdaterWeb implements IJSPropsUpdater {
// noop
}
public removeOnJSPropsChangeListener(
public unregisterComponent(
_animatedComponent: React.Component<
AnimatedComponentProps<InitialComponentProps>
> &
@@ -135,20 +66,23 @@ class JSPropsUpdaterWeb implements IJSPropsUpdater {
) {
// noop
}
public updateProps(_operations: JSPropsOperation[]) {
// noop
}
}
type JSPropsUpdaterOptions =
| typeof JSPropsUpdaterWeb
| typeof JSPropsUpdaterFabric
| typeof JSPropsUpdaterPaper;
| typeof JSPropsUpdaterNative;
let JSPropsUpdater: JSPropsUpdaterOptions;
if (SHOULD_BE_USE_WEB) {
JSPropsUpdater = JSPropsUpdaterWeb;
} else if (global._IS_FABRIC) {
JSPropsUpdater = JSPropsUpdaterFabric;
} else {
JSPropsUpdater = JSPropsUpdaterPaper;
JSPropsUpdater = JSPropsUpdaterNative;
}
export default JSPropsUpdater;
const jsPropsUpdater = new JSPropsUpdater();
export default jsPropsUpdater;
@@ -1,19 +1,19 @@
'use strict';
import type { StyleProps, SharedValue } from '../reanimated2';
import { isSharedValue } from '../reanimated2';
import { isChromeDebugger } from '../reanimated2/PlatformChecker';
import WorkletEventHandler from '../reanimated2/WorkletEventHandler';
import { initialUpdaterRun } from '../reanimated2/animation';
import { hasInlineStyles, getInlineStyle } from './InlinePropManager';
import { initialUpdaterRun } from '../animation';
import type { StyleProps } from '../commonTypes';
import type { AnimatedStyleHandle } from '../hook/commonTypes';
import { isSharedValue } from '../isSharedValue';
import { WorkletEventHandler } from '../WorkletEventHandler';
import type {
AnimatedComponentProps,
AnimatedComponentType,
AnimatedProps,
InitialComponentProps,
IAnimatedComponentInternal,
IPropsFilter,
} from './commonTypes';
import { getInlineStyle, hasInlineStyles } from './InlinePropManager';
import { flattenArray, has } from './utils';
import { StyleSheet } from 'react-native';
function dummyListener() {
// empty listener we use to assign to listener properties for which animated
@@ -21,56 +21,68 @@ function dummyListener() {
}
export class PropsFilter implements IPropsFilter {
private _initialStyle = {};
private _initialPropsMap = new Map<AnimatedStyleHandle, StyleProps>();
public filterNonAnimatedProps(
component: React.Component<unknown, unknown> & IAnimatedComponentInternal
component: AnimatedComponentType
): Record<string, unknown> {
const inputProps =
component.props as AnimatedComponentProps<InitialComponentProps>;
const props: Record<string, unknown> = {};
for (const key in inputProps) {
const value = inputProps[key];
if (key === 'style') {
const styleProp = inputProps.style;
const styles = flattenArray<StyleProps>(styleProp ?? []);
const processedStyle: StyleProps = styles.map((style) => {
if (style && style.viewDescriptors) {
// this is how we recognize styles returned by useAnimatedStyle
style.viewsRef.add(component);
const processedStyle: StyleProps[] = styles.map((style) => {
if (style?.viewDescriptors) {
const handle = style as AnimatedStyleHandle;
if (component._isFirstRender) {
this._initialStyle = {
...style.initial.value,
...this._initialStyle,
...initialUpdaterRun<StyleProps>(style.initial.updater),
};
this._initialPropsMap.set(handle, {
...handle.initial.value,
...initialUpdaterRun(handle.initial.updater),
} as StyleProps);
}
return this._initialStyle;
return this._initialPropsMap.get(handle) ?? {};
} else if (hasInlineStyles(style)) {
return getInlineStyle(style, component._isFirstRender);
} else {
return style;
}
});
props[key] = StyleSheet.flatten(processedStyle);
// keep styles as they were passed by the user
// it will help other libs to interpret styles correctly
props[key] = processedStyle;
} else if (key === 'animatedProps') {
const animatedProp = inputProps.animatedProps as Partial<
AnimatedComponentProps<AnimatedProps>
>;
if (animatedProp.initial !== undefined) {
Object.keys(animatedProp.initial.value).forEach((key) => {
props[key] = animatedProp.initial?.value[key];
animatedProp.viewsRef?.add(component);
});
}
const animatedPropsProp = inputProps.animatedProps;
const animatedPropsArray = flattenArray<
Partial<AnimatedComponentProps<AnimatedProps>>
>(animatedPropsProp ?? []);
animatedPropsArray.forEach((animatedProps) => {
if (animatedProps?.viewDescriptors && animatedProps.initial) {
Object.keys(animatedProps.initial.value).forEach(
(initialValueKey) => {
props[initialValueKey] =
animatedProps.initial?.value[initialValueKey];
}
);
}
});
} else if (
has('current', value) &&
value.current instanceof WorkletEventHandler
has('workletEventHandler', value) &&
value.workletEventHandler instanceof WorkletEventHandler
) {
if (value.current.eventNames.length > 0) {
value.current.eventNames.forEach((eventName) => {
props[eventName] = has('listeners', value.current)
? (value.current.listeners as Record<string, unknown>)[eventName]
if (value.workletEventHandler.eventNames.length > 0) {
value.workletEventHandler.eventNames.forEach((eventName) => {
props[eventName] = has('listeners', value.workletEventHandler)
? (
value.workletEventHandler.listeners as Record<string, unknown>
)[eventName]
: dummyListener;
});
} else {
@@ -78,9 +90,9 @@ export class PropsFilter implements IPropsFilter {
}
} else if (isSharedValue(value)) {
if (component._isFirstRender) {
props[key] = (value as SharedValue<unknown>).value;
props[key] = value.value;
}
} else if (key !== 'onGestureHandlerStateChange' || !isChromeDebugger()) {
} else {
props[key] = value;
}
}
@@ -1,32 +1,35 @@
'use strict';
import type { Ref, Component } from 'react';
import type { Component, Ref, RefObject } from 'react';
import type {
StyleProps,
BaseAnimationBuilder,
ILayoutAnimationBuilder,
AnimatedStyle,
EntryExitAnimationFunction,
SharedTransition,
ILayoutAnimationBuilder,
ShadowNodeWrapper,
SharedValue,
} from '../reanimated2';
import type {
ViewDescriptorsSet,
ViewRefSet,
} from '../reanimated2/ViewDescriptorsSet';
import type { SkipEnteringContext } from '../reanimated2/component/LayoutAnimationConfig';
import type { ShadowNodeWrapper } from '../reanimated2/commonTypes';
import type { ViewConfig } from '../ConfigHelper';
StyleProps,
StyleUpdaterContainer,
} from '../commonTypes';
import type { SkipEnteringContext } from '../component/LayoutAnimationConfig';
import type { BaseAnimationBuilder } from '../layoutReanimation';
import type { ViewDescriptorsSet } from '../ViewDescriptorsSet';
export interface AnimatedProps extends Record<string, unknown> {
viewDescriptors?: ViewDescriptorsSet;
viewsRef?: ViewRefSet<unknown>;
initial?: SharedValue<StyleProps>;
styleUpdaterContainer?: StyleUpdaterContainer;
}
export interface ViewInfo {
viewTag: number | HTMLElement | null;
viewName: string | null;
viewTag: number | AnimatedComponentRef | HTMLElement | null;
shadowNodeWrapper: ShadowNodeWrapper | null;
viewConfig: ViewConfig;
// This is a React host instance view name which might differ from the
// Fabric component name. For clarity, we use the viewName property
// here and componentName in C++ after converting react viewName to
// Fabric component name.
// (see react/renderer/componentregistry/componentNameByReactViewName.cpp)
viewName?: string;
DOMElement?: HTMLElement | null;
}
export interface IInlinePropManager {
@@ -37,31 +40,49 @@ export interface IInlinePropManager {
detachInlineProps(): void;
}
export type AnimatedComponentType = React.Component<unknown, unknown> &
IAnimatedComponentInternal;
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
export type PropUpdates = StyleProps | AnimatedStyle<any>;
export interface IPropsFilter {
filterNonAnimatedProps: (
component: React.Component<unknown, unknown> & IAnimatedComponentInternal
component: AnimatedComponentType
) => Record<string, unknown>;
}
export type JSPropsOperation = {
tag: number;
updates: StyleProps;
};
export interface IJSPropsUpdater {
addOnJSPropsChangeListener(
animatedComponent: React.Component<unknown, unknown> &
IAnimatedComponentInternal
): void;
removeOnJSPropsChangeListener(
animatedComponent: React.Component<unknown, unknown> &
IAnimatedComponentInternal
registerComponent(
animatedComponent: AnimatedComponentType,
jsProps: string[]
): void;
unregisterComponent(animatedComponent: AnimatedComponentType): void;
updateProps(operations: JSPropsOperation[]): void;
}
export interface INativeEventsManager {
attachEvents(): void;
detachEvents(): void;
updateEvents(prevProps: AnimatedComponentProps<InitialComponentProps>): void;
}
export type LayoutAnimationStaticContext = {
presetName: string;
};
export type AnimatedComponentProps<P extends Record<string, unknown>> = P & {
forwardedRef?: Ref<Component>;
export type AnimatedComponentProps<
P extends Record<string, unknown> = Record<string, unknown>,
> = P & {
ref?: Ref<Component>;
style?: NestedArray<StyleProps>;
animatedProps?: Partial<AnimatedComponentProps<AnimatedProps>>;
jestAnimatedValues?: RefObject<AnimatedProps>;
animatedStyle?: StyleProps;
layout?: (
| BaseAnimationBuilder
@@ -83,34 +104,67 @@ export type AnimatedComponentProps<P extends Record<string, unknown>> = P & {
| Keyframe
) &
LayoutAnimationStaticContext;
sharedTransitionTag?: string;
sharedTransitionStyle?: SharedTransition;
};
export type LayoutAnimationOrBuilder = (
| BaseAnimationBuilder
| typeof BaseAnimationBuilder
| EntryExitAnimationFunction
| Keyframe
| ILayoutAnimationBuilder
) &
LayoutAnimationStaticContext;
export interface AnimatedComponentRef extends Component {
setNativeProps?: (props: Record<string, unknown>) => void;
getScrollableNode?: () => AnimatedComponentRef;
getAnimatableRef?: () => AnimatedComponentRef;
// Case for SVG components on Web
elementRef?: React.RefObject<HTMLElement>;
}
export interface IAnimatedComponentInternal {
_styles: StyleProps[] | null;
_animatedProps?: Partial<AnimatedComponentProps<AnimatedProps>>;
_viewTag: number;
export interface IAnimatedComponentInternalBase {
ChildComponent: AnyComponent;
_componentRef: AnimatedComponentRef | HTMLElement | null;
_hasAnimatedRef: boolean;
_viewInfo?: ViewInfo;
/**
* Used for Layout Animations and Animated Styles. It is not related to event
* handling.
*/
getComponentViewTag: () => number;
}
export interface IAnimatedComponentInternal
extends IAnimatedComponentInternalBase {
_animatedStyles: StyleProps[];
_prevAnimatedStyles: StyleProps[];
_animatedProps: Partial<AnimatedComponentProps<AnimatedProps>>[];
_prevAnimatedProps: Partial<AnimatedComponentProps<AnimatedProps>>[];
_isFirstRender: boolean;
animatedStyle: { value: StyleProps };
_component: AnimatedComponentRef | HTMLElement | null;
_sharedElementTransition: SharedTransition | null;
_jsPropsUpdater: IJSPropsUpdater;
jestInlineStyle: NestedArray<StyleProps> | undefined;
jestAnimatedStyle: { value: StyleProps };
jestAnimatedProps: { value: AnimatedProps };
_InlinePropManager: IInlinePropManager;
_PropsFilter: IPropsFilter;
_viewInfo?: ViewInfo;
/** Doesn't exist on web. */
_NativeEventsManager?: INativeEventsManager;
context: React.ContextType<typeof SkipEnteringContext>;
setNativeProps: (props: StyleProps) => void;
}
export type NestedArray<T> = T | NestedArray<T>[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyComponent = React.ComponentType<any>;
export interface InitialComponentProps extends Record<string, unknown> {
ref?: Ref<Component>;
collapsable?: boolean;
}
export type ManagedAnimatedComponent = React.Component<
AnimatedComponentProps<InitialComponentProps>
> &
IAnimatedComponentInternal;
@@ -1,81 +1,24 @@
'use strict';
import type {
Component,
ComponentClass,
ComponentType,
FunctionComponent,
MutableRefObject,
Ref,
} from 'react';
import React from 'react';
import { findNodeHandle, Platform } from 'react-native';
import WorkletEventHandler from '../reanimated2/WorkletEventHandler';
import '../reanimated2/layoutReanimation/animationsManager';
import invariant from 'invariant';
import { adaptViewConfig } from '../ConfigHelper';
import { RNRenderer } from '../reanimated2/platform-specific/RNRenderer';
import {
configureLayoutAnimations,
enableLayoutAnimations,
} from '../reanimated2/core';
import {
SharedTransition,
LayoutAnimationType,
} from '../reanimated2/layoutReanimation';
import type { StyleProps, ShadowNodeWrapper } from '../reanimated2/commonTypes';
import { getShadowNodeWrapperFromRef } from '../reanimated2/fabricUtils';
import { removeFromPropsRegistry } from '../reanimated2/PropsRegistry';
import { getReduceMotionFromConfig } from '../reanimated2/animation/util';
import { maybeBuild } from '../animationBuilder';
import { SkipEnteringContext } from '../reanimated2/component/LayoutAnimationConfig';
import type { AnimateProps } from '../reanimated2';
import JSPropsUpdater from './JSPropsUpdater';
import type {
AnimatedComponentProps,
AnimatedProps,
InitialComponentProps,
AnimatedComponentRef,
IAnimatedComponentInternal,
ViewInfo,
} from './commonTypes';
import { has, flattenArray } from './utils';
import setAndForwardRef from './setAndForwardRef';
import {
isFabric,
isJest,
isWeb,
shouldBeUseWeb,
} from '../reanimated2/PlatformChecker';
import { InlinePropManager } from './InlinePropManager';
import { PropsFilter } from './PropsFilter';
import {
startWebLayoutAnimation,
tryActivateLayoutTransition,
configureWebLayoutAnimations,
getReducedMotionFromConfig,
} from '../reanimated2/layoutReanimation/web';
import type { CustomConfig } from '../reanimated2/layoutReanimation/web/config';
import type { FlatList, FlatListProps } from 'react-native';
const IS_WEB = isWeb();
const IS_FABRIC = isFabric();
import type { AnimatedProps } from '../helperTypes';
import type { Options } from './AnimatedComponent';
import AnimatedComponentImpl from './AnimatedComponent';
import type {
AnimatedComponentProps,
InitialComponentProps,
} from './commonTypes';
function onlyAnimatedStyles(styles: StyleProps[]): StyleProps[] {
return styles.filter((style) => style?.viewDescriptors);
}
function isSameAnimatedStyle(
style1?: StyleProps,
style2?: StyleProps
): boolean {
// We cannot use equality check to compare useAnimatedStyle outputs directly.
// Instead, we can compare its viewsRefs.
return style1?.viewsRef === style2?.viewsRef;
}
const isSameAnimatedProps = isSameAnimatedStyle;
type Options<P> = {
setNativeProps: (ref: AnimatedComponentRef, props: P) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnimatableComponent<C extends ComponentType<any>> = C & {
jsProps?: string[];
};
/**
@@ -86,517 +29,70 @@ type Options<P> = {
* @see https://docs.swmansion.com/react-native-reanimated/docs/core/createAnimatedComponent
*/
// Don't change the order of overloads, since such a change breaks current behavior
export function createAnimatedComponent<P extends object>(
component: AnimatableComponent<FunctionComponent<P>>,
options?: Options<P>
): FunctionComponent<AnimatedProps<P>>;
export function createAnimatedComponent<P extends object>(
component: AnimatableComponent<ComponentClass<P>>,
options?: Options<P>
): ComponentClass<AnimatedProps<P>>;
export function createAnimatedComponent<P extends object>(
// Actually ComponentType<P = {}> = ComponentClass<P> | FunctionComponent<P> but we need this overload too
// since some external components (like FastImage) are typed just as ComponentType
component: AnimatableComponent<ComponentType<P>>,
options?: Options<P>
): FunctionComponent<AnimatedProps<P>> | ComponentClass<AnimatedProps<P>>;
/**
* @deprecated Please use `Animated.FlatList` component instead of calling `Animated.createAnimatedComponent(FlatList)` manually.
* @deprecated Please use `Animated.FlatList` component instead of calling
* `Animated.createAnimatedComponent(FlatList)` manually.
*/
// @ts-ignore This is required to create this overload, since type of createAnimatedComponent is incorrect and doesn't include typeof FlatList
export function createAnimatedComponent(
component: typeof FlatList<unknown>,
options?: Options<any>
): ComponentClass<AnimateProps<FlatListProps<unknown>>>;
export function createAnimatedComponent<P extends object>(
component: FunctionComponent<P>,
options?: Options<P>
): FunctionComponent<AnimateProps<P>>;
export function createAnimatedComponent<P extends object>(
component: ComponentClass<P>,
options?: Options<P>
): ComponentClass<AnimateProps<P>>;
component: AnimatableComponent<typeof FlatList<unknown>>,
options?: Options<typeof FlatList<unknown>>
): ComponentClass<AnimatedProps<FlatListProps<unknown>>>;
export function createAnimatedComponent(
Component: ComponentType<InitialComponentProps>,
Component: AnimatableComponent<ComponentType<InitialComponentProps>>,
options?: Options<InitialComponentProps>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): any {
invariant(
typeof Component !== 'function' ||
(Component.prototype && Component.prototype.isReactComponent),
`Looks like you're passing a function component \`${Component.name}\` to \`createAnimatedComponent\` function which supports only class components. Please wrap your function component with \`React.forwardRef()\` or use a class component instead.`
);
class AnimatedComponent
extends React.Component<AnimatedComponentProps<InitialComponentProps>>
implements IAnimatedComponentInternal
{
_styles: StyleProps[] | null = null;
_animatedProps?: Partial<AnimatedComponentProps<AnimatedProps>>;
_viewTag = -1;
_isFirstRender = true;
animatedStyle: { value: StyleProps } = { value: {} };
_component: AnimatedComponentRef | HTMLElement | null = null;
_sharedElementTransition: SharedTransition | null = null;
_jsPropsUpdater = new JSPropsUpdater();
_InlinePropManager = new InlinePropManager();
_PropsFilter = new PropsFilter();
_viewInfo?: ViewInfo;
static displayName: string;
static contextType = SkipEnteringContext;
context!: React.ContextType<typeof SkipEnteringContext>;
class AnimatedComponent extends AnimatedComponentImpl {
static displayName = `AnimatedComponent(${
Component.displayName || Component.name || 'Component'
})`;
constructor(props: AnimatedComponentProps<InitialComponentProps>) {
super(props);
if (isJest()) {
this.animatedStyle = { value: {} };
}
}
componentDidMount() {
this._attachNativeEvents();
this._jsPropsUpdater.addOnJSPropsChangeListener(this);
this._attachAnimatedStyles();
this._InlinePropManager.attachInlineProps(this, this._getViewInfo());
if (IS_WEB) {
configureWebLayoutAnimations();
if (!this.props.entering) {
this._isFirstRender = false;
return;
}
if (getReducedMotionFromConfig(this.props.entering as CustomConfig)) {
this._isFirstRender = false;
return;
}
startWebLayoutAnimation(
this.props,
this._component as HTMLElement,
LayoutAnimationType.ENTERING
);
}
this._isFirstRender = false;
}
componentWillUnmount() {
this._detachNativeEvents();
this._jsPropsUpdater.removeOnJSPropsChangeListener(this);
this._detachStyles();
this._InlinePropManager.detachInlineProps();
this._sharedElementTransition?.unregisterTransition(this._viewTag);
if (
IS_WEB &&
this.props.exiting &&
!getReducedMotionFromConfig(this.props.exiting as CustomConfig)
) {
startWebLayoutAnimation(
this.props,
this._component as HTMLElement,
LayoutAnimationType.EXITING
);
}
}
_getEventViewRef() {
// Make sure to get the scrollable node for components that implement
// `ScrollResponder.Mixin`.
return (this._component as AnimatedComponentRef)?.getScrollableNode
? (this._component as AnimatedComponentRef).getScrollableNode?.()
: this._component;
}
_attachNativeEvents() {
const node = this._getEventViewRef() as AnimatedComponentRef;
let viewTag = null; // We set it only if needed
for (const key in this.props) {
const prop = this.props[key];
if (
has('current', prop) &&
prop.current instanceof WorkletEventHandler
) {
if (viewTag === null) {
viewTag = findNodeHandle(options?.setNativeProps ? this : node);
}
prop.current.registerForEvents(viewTag as number, key);
}
}
}
_detachNativeEvents() {
for (const key in this.props) {
const prop = this.props[key];
if (
has('current', prop) &&
prop.current instanceof WorkletEventHandler
) {
prop.current.unregisterFromEvents();
}
}
}
_detachStyles() {
if (IS_WEB && this._styles !== null) {
for (const style of this._styles) {
if (style?.viewsRef) {
style.viewsRef.remove(this);
}
}
} else if (this._viewTag !== -1 && this._styles !== null) {
for (const style of this._styles) {
style.viewDescriptors.remove(this._viewTag);
}
if (this.props.animatedProps?.viewDescriptors) {
this.props.animatedProps.viewDescriptors.remove(this._viewTag);
}
if (IS_FABRIC) {
removeFromPropsRegistry(this._viewTag);
}
}
}
_reattachNativeEvents(
prevProps: AnimatedComponentProps<InitialComponentProps>
) {
for (const key in prevProps) {
const prop = this.props[key];
if (
has('current', prop) &&
prop.current instanceof WorkletEventHandler &&
prop.current.reattachNeeded
) {
prop.current.unregisterFromEvents();
}
}
let viewTag = null;
for (const key in this.props) {
const prop = this.props[key];
if (
has('current', prop) &&
prop.current instanceof WorkletEventHandler &&
prop.current.reattachNeeded
) {
if (viewTag === null) {
const node = this._getEventViewRef() as AnimatedComponentRef;
viewTag = findNodeHandle(options?.setNativeProps ? this : node);
}
prop.current.registerForEvents(viewTag as number, key);
prop.current.reattachNeeded = false;
}
}
}
_updateFromNative(props: StyleProps) {
if (options?.setNativeProps) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
options.setNativeProps(this._component as AnimatedComponentRef, props);
} else {
// eslint-disable-next-line no-unused-expressions
(this._component as AnimatedComponentRef)?.setNativeProps?.(props);
}
}
_getViewInfo(): ViewInfo {
if (this._viewInfo !== undefined) {
return this._viewInfo;
}
let viewTag: number | HTMLElement | null;
let viewName: string | null;
let shadowNodeWrapper: ShadowNodeWrapper | null = null;
let viewConfig;
// Component can specify ref which should be animated when animated version of the component is created.
// Otherwise, we animate the component itself.
const component = (this._component as AnimatedComponentRef)
?.getAnimatableRef
? (this._component as AnimatedComponentRef).getAnimatableRef?.()
: this;
if (IS_WEB) {
// At this point I assume that `_setComponentRef` was already called and `_component` is set.
// `this._component` on web represents HTMLElement of our component, that's why we use casting
viewTag = this._component as HTMLElement;
viewName = null;
shadowNodeWrapper = null;
viewConfig = null;
} else {
// hostInstance can be null for a component that doesn't render anything (render function returns null). Example: svg Stop: https://github.com/react-native-svg/react-native-svg/blob/develop/src/elements/Stop.tsx
const hostInstance = RNRenderer.findHostInstance_DEPRECATED(component);
if (!hostInstance) {
throw new Error(
'[Reanimated] Cannot find host instance for this component. Maybe it renders nothing?'
);
}
// we can access view tag in the same way it's accessed here https://github.com/facebook/react/blob/e3f4eb7272d4ca0ee49f27577156b57eeb07cf73/packages/react-native-renderer/src/ReactFabric.js#L146
viewTag = hostInstance?._nativeTag;
/**
* RN uses viewConfig for components for storing different properties of the component(example: https://github.com/facebook/react-native/blob/main/packages/react-native/Libraries/Components/ScrollView/ScrollViewNativeComponent.js#L24).
* The name we're looking for is in the field named uiViewClassName.
*/
viewName = hostInstance?.viewConfig?.uiViewClassName;
viewConfig = hostInstance?.viewConfig;
if (IS_FABRIC) {
shadowNodeWrapper = getShadowNodeWrapperFromRef(this);
}
}
this._viewInfo = { viewTag, viewName, shadowNodeWrapper, viewConfig };
return this._viewInfo;
}
_attachAnimatedStyles() {
const styles = this.props.style
? onlyAnimatedStyles(flattenArray<StyleProps>(this.props.style))
: [];
const prevStyles = this._styles;
this._styles = styles;
const prevAnimatedProps = this._animatedProps;
this._animatedProps = this.props.animatedProps;
const { viewTag, viewName, shadowNodeWrapper, viewConfig } =
this._getViewInfo();
// update UI props whitelist for this view
const hasReanimated2Props =
this.props.animatedProps?.viewDescriptors || styles.length;
if (hasReanimated2Props && viewConfig) {
adaptViewConfig(viewConfig);
}
this._viewTag = viewTag as number;
// remove old styles
if (prevStyles) {
// in most of the cases, views have only a single animated style and it remains unchanged
const hasOneSameStyle =
styles.length === 1 &&
prevStyles.length === 1 &&
isSameAnimatedStyle(styles[0], prevStyles[0]);
if (!hasOneSameStyle) {
// otherwise, remove each style that is not present in new styles
for (const prevStyle of prevStyles) {
const isPresent = styles.some((style) =>
isSameAnimatedStyle(style, prevStyle)
);
if (!isPresent) {
prevStyle.viewDescriptors.remove(viewTag);
}
}
}
}
styles.forEach((style) => {
style.viewDescriptors.add({
tag: viewTag,
name: viewName,
shadowNodeWrapper,
});
if (isJest()) {
/**
* We need to connect Jest's TestObject instance whose contains just props object
* with the updateProps() function where we update the properties of the component.
* We can't update props object directly because TestObject contains a copy of props - look at render function:
* const props = this._filterNonAnimatedProps(this.props);
*/
this.animatedStyle.value = {
...this.animatedStyle.value,
...style.initial.value,
};
style.animatedStyle.current = this.animatedStyle;
}
});
// detach old animatedProps
if (
prevAnimatedProps &&
!isSameAnimatedProps(prevAnimatedProps, this.props.animatedProps)
) {
prevAnimatedProps.viewDescriptors!.remove(viewTag as number);
}
// attach animatedProps property
if (this.props.animatedProps?.viewDescriptors) {
this.props.animatedProps.viewDescriptors.add({
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
tag: viewTag as number,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
name: viewName!,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
shadowNodeWrapper: shadowNodeWrapper!,
});
}
}
componentDidUpdate(
prevProps: AnimatedComponentProps<InitialComponentProps>,
_prevState: Readonly<unknown>,
// This type comes straight from React
// eslint-disable-next-line @typescript-eslint/no-explicit-any
snapshot: DOMRect | null
) {
this._reattachNativeEvents(prevProps);
this._attachAnimatedStyles();
this._InlinePropManager.attachInlineProps(this, this._getViewInfo());
// Snapshot won't be undefined because it comes from getSnapshotBeforeUpdate method
if (
IS_WEB &&
snapshot !== null &&
this.props.layout &&
!getReducedMotionFromConfig(this.props.layout as CustomConfig)
) {
tryActivateLayoutTransition(
this.props,
this._component as HTMLElement,
snapshot
);
}
}
_setComponentRef = setAndForwardRef<Component | HTMLElement>({
getForwardedRef: () =>
this.props.forwardedRef as MutableRefObject<
Component<Record<string, unknown>, Record<string, unknown>, unknown>
>,
setLocalRef: (ref) => {
// TODO update config
const tag = IS_WEB
? (ref as HTMLElement)
: findNodeHandle(ref as Component);
const { layout, entering, exiting, sharedTransitionTag } = this.props;
if (
(layout || entering || exiting || sharedTransitionTag) &&
tag != null
) {
if (!shouldBeUseWeb()) {
enableLayoutAnimations(true, false);
}
if (layout) {
configureLayoutAnimations(
tag,
LayoutAnimationType.LAYOUT,
maybeBuild(
layout,
undefined /* We don't have to warn user if style has common properties with animation for LAYOUT */,
AnimatedComponent.displayName
)
);
}
const skipEntering = this.context?.current;
if (entering && !skipEntering) {
configureLayoutAnimations(
tag,
LayoutAnimationType.ENTERING,
maybeBuild(
entering,
this.props?.style,
AnimatedComponent.displayName
)
);
}
if (exiting) {
const reduceMotionInExiting =
'getReduceMotion' in exiting &&
typeof exiting.getReduceMotion === 'function'
? getReduceMotionFromConfig(exiting.getReduceMotion())
: getReduceMotionFromConfig();
if (!reduceMotionInExiting) {
configureLayoutAnimations(
tag,
LayoutAnimationType.EXITING,
maybeBuild(
exiting,
this.props?.style,
AnimatedComponent.displayName
)
);
}
}
if (sharedTransitionTag && !IS_WEB) {
const sharedElementTransition =
this.props.sharedTransitionStyle ?? new SharedTransition();
const reduceMotionInTransition = getReduceMotionFromConfig(
sharedElementTransition.getReduceMotion()
);
if (!reduceMotionInTransition) {
sharedElementTransition.registerTransition(
tag as number,
sharedTransitionTag
);
this._sharedElementTransition = sharedElementTransition;
}
}
}
if (ref !== this._component) {
this._component = ref;
}
},
});
// This is a component lifecycle method from React, therefore we are not calling it directly.
// It is called before the component gets rerendered. This way we can access components' position before it changed
// and later on, in componentDidUpdate, calculate translation for layout transition.
getSnapshotBeforeUpdate() {
if (
(this._component as HTMLElement).getBoundingClientRect !== undefined
) {
return (this._component as HTMLElement).getBoundingClientRect();
}
return null;
}
render() {
const props = this._PropsFilter.filterNonAnimatedProps(this);
if (isJest()) {
props.animatedStyle = this.animatedStyle;
}
// Layout animations on web are set inside `componentDidMount` method, which is called after first render.
// Because of that we can encounter a situation in which component is visible for a short amount of time, and later on animation triggers.
// I've tested that on various browsers and devices and it did not happen to me. To be sure that it won't happen to someone else,
// I've decided to hide component at first render. Its visibility is reset in `componentDidMount`.
if (
this._isFirstRender &&
IS_WEB &&
props.entering &&
!getReducedMotionFromConfig(props.entering as CustomConfig)
) {
props.style = {
...(props.style ?? {}),
visibility: 'hidden', // Hide component until `componentDidMount` triggers
};
}
const platformProps = Platform.select({
web: {},
default: { collapsable: false },
});
return (
<Component
{...props}
// Casting is used here, because ref can be null - in that case it cannot be assigned to HTMLElement.
// After spending some time trying to figure out what to do with this problem, we decided to leave it this way
ref={this._setComponentRef as (ref: Component) => void}
{...platformProps}
/>
);
// User can override component-defined jsProps via options
const jsProps = options?.jsProps ?? Component.jsProps;
const modifiedOptions = jsProps?.length
? { ...options, jsProps }
: options;
super(Component, props, AnimatedComponent.displayName, modifiedOptions);
}
}
AnimatedComponent.displayName = `AnimatedComponent(${
Component.displayName || Component.name || 'Component'
})`;
return React.forwardRef<Component>((props, ref) => {
const animatedComponent = (
props: AnimatedComponentProps & { ref: Ref<AnimatedComponent> }
) => {
return (
<AnimatedComponent
{...props}
{...(ref === null ? null : { forwardedRef: ref })}
// Needed to prevent react from signing AnimatedComponent to the ref
// (we want to handle the ref assignment in the AnimatedComponent)
ref={null}
{...(props.ref === null ? null : { forwardedRef: props.ref })}
/>
);
});
};
animatedComponent.displayName =
Component.displayName || Component.name || 'Component';
return animatedComponent;
}
@@ -1,4 +1,6 @@
'use strict';
import type { StyleProps } from '../commonTypes';
import type { CSSStyle } from '../css';
import type { NestedArray } from './commonTypes';
export function flattenArray<T>(array: NestedArray<T>): T[] {
@@ -23,7 +25,7 @@ export function flattenArray<T>(array: NestedArray<T>): T[] {
export const has = <K extends string>(
key: K,
x: unknown
): x is typeof x & { [key in K]: unknown } => {
): x is { [key in K]: unknown } => {
if (typeof x === 'function' || typeof x === 'object') {
if (x === null || x === undefined) {
return false;
@@ -33,3 +35,26 @@ export const has = <K extends string>(
}
return false;
};
type FilteredStyles = {
cssStyle: CSSStyle | null;
animatedStyles: StyleProps[];
};
export function filterStyles(styles: StyleProps[] | undefined): FilteredStyles {
if (!styles) {
return { animatedStyles: [], cssStyle: null };
}
return styles.reduce<FilteredStyles>(
({ animatedStyles, cssStyle }, style) => {
if (style?.viewDescriptors) {
animatedStyles.push(style);
} else {
cssStyle = { ...cssStyle, ...style } as CSSStyle;
}
return { animatedStyles, cssStyle };
},
{ animatedStyles: [], cssStyle: null }
);
}