chore: update map

This commit is contained in:
2026-02-19 08:55:45 +01:00
parent 3308adf1ea
commit 900c54c20b
1845 changed files with 3677 additions and 84269 deletions
@@ -1,460 +0,0 @@
'use strict';
import '../layoutReanimation/animationsManager';
import type React from 'react';
import { maybeBuild } from '../animationBuilder';
import { IS_JEST, IS_WEB, logger } from '../common';
import type { StyleProps } from '../commonTypes';
import { LayoutAnimationType } from '../commonTypes';
import { SkipEnteringContext } from '../component/LayoutAnimationConfig';
import ReanimatedAnimatedComponent from '../css/component/AnimatedComponent';
import type { AnimatedStyleHandle } from '../hook/commonTypes';
import {
configureWebLayoutAnimations,
getReducedMotionFromConfig,
saveSnapshot,
startWebLayoutAnimation,
tryActivateLayoutTransition,
} from '../layoutReanimation/web';
import type { CustomConfig } from '../layoutReanimation/web/config';
import { addHTMLMutationObserver } from '../layoutReanimation/web/domUtils';
import type { ReanimatedHTMLElement } from '../ReanimatedModule/js-reanimated';
import { updateLayoutAnimations } from '../UpdateLayoutAnimations';
import type {
AnimatedComponentProps,
AnimatedComponentRef,
AnimatedProps,
AnyComponent,
IAnimatedComponentInternal,
INativeEventsManager,
InitialComponentProps,
LayoutAnimationOrBuilder,
NestedArray,
} from './commonTypes';
import { InlinePropManager } from './InlinePropManager';
import jsPropsUpdater from './JSPropsUpdater';
import { NativeEventsManager } from './NativeEventsManager';
import { PropsFilter } from './PropsFilter';
import { filterStyles, flattenArray } from './utils';
let id = 0;
if (IS_WEB) {
configureWebLayoutAnimations();
}
export type Options<P> = {
setNativeProps?: (ref: AnimatedComponentRef, props: P) => void;
jsProps?: string[];
};
export default class AnimatedComponent
extends ReanimatedAnimatedComponent<
AnimatedComponentProps<InitialComponentProps>
>
implements IAnimatedComponentInternal
{
_options?: Options<InitialComponentProps>;
_displayName: string;
_animatedStyles: StyleProps[] = [];
_prevAnimatedStyles: StyleProps[] = [];
_animatedProps: Partial<AnimatedComponentProps<AnimatedProps>>[] = [];
_prevAnimatedProps: Partial<AnimatedComponentProps<AnimatedProps>>[] = [];
_isFirstRender = true;
jestInlineStyle: NestedArray<StyleProps> | undefined;
jestAnimatedStyle: { value: StyleProps } = { value: {} };
jestAnimatedProps: { value: AnimatedProps } = { value: {} };
_InlinePropManager = new InlinePropManager();
_PropsFilter = new PropsFilter();
_NativeEventsManager?: INativeEventsManager;
static contextType = SkipEnteringContext;
context!: React.ContextType<typeof SkipEnteringContext>;
reanimatedID = id++;
constructor(
ChildComponent: AnyComponent,
props: AnimatedComponentProps<InitialComponentProps>,
displayName: string,
options?: Options<InitialComponentProps>
) {
super(ChildComponent, props);
this._options = options;
this._displayName = displayName;
if (IS_JEST) {
this.jestAnimatedStyle = { value: {} };
this.jestAnimatedProps = { value: {} };
}
const skipEntering = this.context?.current;
if (!skipEntering) {
this._configureLayoutAnimation(
LayoutAnimationType.ENTERING,
this.props.entering
);
}
}
componentDidMount() {
super.componentDidMount();
if (!IS_WEB) {
// It exists only on native platforms. We initialize it here because the ref to the animated component is available only post-mount
this._NativeEventsManager = new NativeEventsManager(this, this._options);
}
this._NativeEventsManager?.attachEvents();
this._updateAnimatedStylesAndProps();
this._InlinePropManager.attachInlineProps(this, this._getViewInfo());
if (this._options?.jsProps?.length) {
jsPropsUpdater.registerComponent(this, this._options.jsProps);
}
this._configureLayoutAnimation(
LayoutAnimationType.LAYOUT,
this.props.layout
);
this._configureLayoutAnimation(
LayoutAnimationType.EXITING,
this.props.exiting
);
if (IS_WEB && this._componentDOMRef) {
const element = this._componentDOMRef as ReanimatedHTMLElement;
const dummyClone = element.dummyClone;
// If the element was cloned (because of the exiting animation), we need bring it
// back to the DOM
while (dummyClone?.firstChild) {
element.appendChild(dummyClone.firstChild);
}
delete element.dummyClone;
if (this.props.exiting) {
saveSnapshot(element);
}
if (
!this.props.entering ||
getReducedMotionFromConfig(this.props.entering as CustomConfig)
) {
this._isFirstRender = false;
return;
}
const skipEntering = this.context?.current;
if (!skipEntering) {
startWebLayoutAnimation(
this.props,
element,
LayoutAnimationType.ENTERING
);
} else if (element.style) {
element.style.visibility = 'initial';
}
}
this._isFirstRender = false;
}
componentWillUnmount() {
super.componentWillUnmount();
this._NativeEventsManager?.detachEvents();
this._detachStyles();
this._InlinePropManager.detachInlineProps();
if (this._options?.jsProps?.length) {
jsPropsUpdater.unregisterComponent(this);
}
const exiting = this.props.exiting;
if (
IS_WEB &&
this._componentDOMRef &&
exiting &&
!getReducedMotionFromConfig(exiting as CustomConfig)
) {
addHTMLMutationObserver();
startWebLayoutAnimation(
this.props,
this._componentDOMRef as ReanimatedHTMLElement,
LayoutAnimationType.EXITING
);
}
}
_detachStyles() {
const viewTag = this.getComponentViewTag();
if (viewTag !== -1) {
for (const style of this._animatedStyles) {
style.viewDescriptors.remove(viewTag);
}
if (this.props.animatedProps?.viewDescriptors) {
this.props.animatedProps.viewDescriptors.remove(viewTag);
}
}
}
setNativeProps(props: StyleProps) {
if (this._options?.setNativeProps) {
this._options.setNativeProps(
this._componentRef as AnimatedComponentRef,
props
);
} else {
(this._componentRef as AnimatedComponentRef)?.setNativeProps?.(props);
}
}
_handleAnimatedStylesUpdate(
prevStyles: StyleProps[],
currentStyles: StyleProps[],
jestAnimatedStyleOrProps: { value: StyleProps }
) {
const { viewTag, shadowNodeWrapper } = this._getViewInfo();
const newStyles = new Set<StyleProps>(currentStyles);
const isStyleAttached = (style: StyleProps) =>
style.viewDescriptors.has(viewTag);
// remove old styles
if (prevStyles) {
// in most of the cases, views have only a single animated style and it remains unchanged
const hasOneSameStyle =
currentStyles.length === 1 &&
prevStyles.length === 1 &&
currentStyles[0] === prevStyles[0];
if (hasOneSameStyle && isStyleAttached(prevStyles[0])) {
return;
}
// otherwise, remove each style that is not present in new styles
for (const prevStyle of prevStyles) {
const isPresent = currentStyles.some((style) => {
if (style === prevStyle && isStyleAttached(style)) {
newStyles.delete(style);
return true;
}
return false;
});
if (!isPresent) {
prevStyle.viewDescriptors.remove(viewTag);
}
}
}
newStyles.forEach((style) => {
style.viewDescriptors.add(
{
tag: viewTag,
shadowNodeWrapper,
},
style.styleUpdaterContainer
);
if (IS_JEST) {
/**
* 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);
*/
Object.assign(jestAnimatedStyleOrProps.value, style.initial.value);
style.jestAnimatedValues.current = jestAnimatedStyleOrProps;
}
});
}
_updateAnimatedStylesAndProps() {
this._handleAnimatedStylesUpdate(
this._prevAnimatedStyles,
this._animatedStyles,
this.jestAnimatedStyle
);
this._handleAnimatedStylesUpdate(
this._prevAnimatedProps,
this._animatedProps,
this.jestAnimatedProps
);
}
componentDidUpdate(
prevProps: AnimatedComponentProps<InitialComponentProps>,
_prevState: Readonly<unknown>,
snapshot: DOMRect | null
) {
this._configureLayoutAnimation(
LayoutAnimationType.LAYOUT,
this.props.layout,
prevProps.layout
);
this._configureLayoutAnimation(
LayoutAnimationType.EXITING,
this.props.exiting,
prevProps.exiting
);
this._NativeEventsManager?.updateEvents(prevProps);
this._updateAnimatedStylesAndProps();
this._InlinePropManager.attachInlineProps(this, this._getViewInfo());
if (IS_WEB && this.props.exiting && this._componentDOMRef) {
saveSnapshot(this._componentDOMRef);
}
if (
IS_WEB &&
snapshot &&
this.props.layout &&
!getReducedMotionFromConfig(this.props.layout as CustomConfig)
) {
tryActivateLayoutTransition(
this.props,
this._componentDOMRef as ReanimatedHTMLElement,
snapshot
);
}
}
_updateStyles(props: AnimatedComponentProps<InitialComponentProps>): void {
const filteredStyles = filterStyles(flattenArray(props.style ?? []));
this._prevAnimatedStyles = this._animatedStyles;
this._animatedStyles = filteredStyles.animatedStyles;
const filteredAnimatedProps = filterStyles(
flattenArray(props.animatedProps ?? [])
);
this._prevAnimatedProps = this._animatedProps;
this._animatedProps = filteredAnimatedProps.animatedStyles;
if (filteredAnimatedProps.cssStyle) {
if (__DEV__ && filteredStyles.cssStyle) {
logger.warn(
'AnimatedComponent: CSS properties cannot be used in style and animatedProps at the same time. Using properties from the style object.'
);
this._cssStyle = filteredStyles.cssStyle;
return;
}
// Add all remaining props to cssStyle object
// (e.g. SVG components are styled via top level props, not via style object)
const mergedProps = {
...props,
...filteredAnimatedProps.cssStyle,
};
delete mergedProps.style;
delete mergedProps.animatedProps;
this._cssStyle = mergedProps;
} else {
this._cssStyle = filteredStyles.cssStyle ?? {};
}
}
_configureLayoutAnimation(
type: LayoutAnimationType,
currentConfig: LayoutAnimationOrBuilder | undefined,
previousConfig?: LayoutAnimationOrBuilder
) {
if (IS_WEB || currentConfig === previousConfig) {
return;
}
updateLayoutAnimations(
type === LayoutAnimationType.ENTERING
? this.reanimatedID
: this.getComponentViewTag(),
type,
currentConfig &&
maybeBuild(
currentConfig,
type === LayoutAnimationType.LAYOUT
? undefined /* We don't have to warn user if style has common properties with animation for LAYOUT */
: this.props?.style,
this._displayName
)
);
}
// 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 (
IS_WEB &&
this.props.layout &&
this._componentDOMRef?.getBoundingClientRect
) {
return this._componentDOMRef.getBoundingClientRect();
}
// `getSnapshotBeforeUpdate` has to return value which is not `undefined`.
return null;
}
render() {
const filteredProps = this._PropsFilter.filterNonAnimatedProps(this);
if (IS_JEST) {
filteredProps.jestAnimatedStyle = this.jestAnimatedStyle;
filteredProps.jestAnimatedProps = this.jestAnimatedProps;
}
// 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 &&
filteredProps.entering &&
!getReducedMotionFromConfig(filteredProps.entering as CustomConfig)
) {
filteredProps.style = Array.isArray(filteredProps.style)
? filteredProps.style.concat([{ visibility: 'hidden' }])
: {
...(filteredProps.style ?? {}),
visibility: 'hidden', // Hide component until `componentDidMount` triggers
};
}
const skipEntering = this.context?.current;
const nativeID = skipEntering ? undefined : `${this.reanimatedID}`;
const jestProps = IS_JEST
? {
jestInlineStyle:
this.props.style && filterOutAnimatedStyles(this.props.style),
jestAnimatedStyle: this.jestAnimatedStyle,
jestAnimatedProps: this.jestAnimatedProps,
}
: {};
return super.render({
nativeID,
...filteredProps,
...jestProps,
});
}
}
function filterOutAnimatedStyles(
style: NestedArray<StyleProps | AnimatedStyleHandle | null | undefined>
): NestedArray<StyleProps | null | undefined> {
if (!style) {
return style;
}
if (!Array.isArray(style)) {
return style?.viewDescriptors ? {} : style;
}
return style
.filter(
(styleElement) => !(styleElement && 'viewDescriptors' in styleElement)
)
.map((styleElement) => {
if (Array.isArray(styleElement)) {
return filterOutAnimatedStyles(styleElement);
}
return styleElement;
});
}
@@ -1,17 +1,24 @@
'use strict';
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 { StyleProps } from '../reanimated2';
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)) {
@@ -30,30 +37,29 @@ 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(styleValue: StyleProps): unknown {
function getInlinePropsUpdate(inlineProps: Record<string, unknown>) {
'worklet';
if (isSharedValue(styleValue)) {
return styleValue.value;
}
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);
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;
}
return update;
}
return styleValue;
return update;
}
function extractSharedValuesMapFromProps(
@@ -71,14 +77,14 @@ function extractSharedValuesMapFromProps(
if (!style) {
return;
}
for (const [styleKey, styleValue] of Object.entries(style)) {
for (const [key, styleValue] of Object.entries(style)) {
if (isSharedValue(styleValue)) {
inlineProps[styleKey] = styleValue;
inlineProps[key] = styleValue;
} else if (
styleKey === 'transform' &&
key === 'transform' &&
isInlineStyleTransform(styleValue)
) {
inlineProps[styleKey] = styleValue;
inlineProps[key] = styleValue;
}
}
});
@@ -108,7 +114,7 @@ export function getInlineStyle(
isFirstRender: boolean
) {
if (isFirstRender) {
return getInlinePropsUpdate(style) as Record<string, unknown>;
return getInlinePropsUpdate(style);
}
const newStyle: StyleProps = {};
for (const [key, styleValue] of Object.entries(style)) {
@@ -128,7 +134,8 @@ export class InlinePropManager implements IInlinePropManager {
_inlineProps: StyleProps = {};
public attachInlineProps(
animatedComponent: AnimatedComponentType,
animatedComponent: React.Component<unknown, unknown> &
IAnimatedComponentInternal,
viewInfo: ViewInfo
) {
const newInlineProps: Record<string, unknown> =
@@ -139,20 +146,31 @@ export class InlinePropManager implements IInlinePropManager {
if (!this._inlinePropsViewDescriptors) {
this._inlinePropsViewDescriptors = makeViewDescriptorsSet();
const { viewTag, shadowNodeWrapper } = viewInfo;
const { viewTag, viewName, shadowNodeWrapper, viewConfig } = viewInfo;
if (Object.keys(newInlineProps).length && viewConfig) {
adaptViewConfig(viewConfig);
}
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);
updateProps(shareableViewDescriptors, update, maybeViewRef);
};
this._inlineProps = newInlineProps;
if (this._inlinePropsMapperId) {
@@ -1,55 +1,124 @@
'use strict';
import { runOnUI } from 'react-native-worklets';
import { SHOULD_BE_USE_WEB } from '../common';
import {
NativeEventEmitter,
NativeModules,
findNodeHandle,
} from 'react-native';
import { shouldBeUseWeb } from '../reanimated2/PlatformChecker';
import type { StyleProps } from '../reanimated2';
import { runOnJS, runOnUIImmediately } from '../reanimated2/threads';
import type {
AnimatedComponentProps,
AnimatedComponentType,
IAnimatedComponentInternal,
IJSPropsUpdater,
InitialComponentProps,
JSPropsOperation,
} from './commonTypes';
class JSPropsUpdaterNative implements IJSPropsUpdater {
private static _tagToComponentMapping = new Map<
number,
AnimatedComponentType
>();
interface ListenerData {
viewTag: number;
props: StyleProps;
}
public registerComponent(
animatedComponent: AnimatedComponentType,
jsProps: string[]
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
) {
const viewTag = animatedComponent.getComponentViewTag();
JSPropsUpdaterNative._tagToComponentMapping.set(viewTag, animatedComponent);
runOnUI(() => {
global._tagToJSPropNamesMapping[viewTag] = Object.fromEntries(
jsProps.map((propName) => [propName, true])
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
);
})();
}
}
public unregisterComponent(animatedComponent: AnimatedComponentType) {
const viewTag = animatedComponent.getComponentViewTag();
JSPropsUpdaterNative._tagToComponentMapping.delete(viewTag);
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'
);
}
}
}
runOnUI(() => {
delete global._tagToJSPropNamesMapping[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;
}
}
public updateProps(operations: JSPropsOperation[]) {
operations.forEach(({ tag, updates }) => {
const component = JSPropsUpdaterNative._tagToComponentMapping.get(tag);
component?.setNativeProps(updates);
});
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);
}
}
class JSPropsUpdaterWeb implements IJSPropsUpdater {
public registerComponent(
public addOnJSPropsChangeListener(
_animatedComponent: React.Component<
AnimatedComponentProps<InitialComponentProps>
> &
@@ -58,7 +127,7 @@ class JSPropsUpdaterWeb implements IJSPropsUpdater {
// noop
}
public unregisterComponent(
public removeOnJSPropsChangeListener(
_animatedComponent: React.Component<
AnimatedComponentProps<InitialComponentProps>
> &
@@ -66,23 +135,20 @@ class JSPropsUpdaterWeb implements IJSPropsUpdater {
) {
// noop
}
public updateProps(_operations: JSPropsOperation[]) {
// noop
}
}
type JSPropsUpdaterOptions =
| typeof JSPropsUpdaterWeb
| typeof JSPropsUpdaterNative;
| typeof JSPropsUpdaterFabric
| typeof JSPropsUpdaterPaper;
let JSPropsUpdater: JSPropsUpdaterOptions;
if (SHOULD_BE_USE_WEB) {
JSPropsUpdater = JSPropsUpdaterWeb;
} else if (global._IS_FABRIC) {
JSPropsUpdater = JSPropsUpdaterFabric;
} else {
JSPropsUpdater = JSPropsUpdaterNative;
JSPropsUpdater = JSPropsUpdaterPaper;
}
const jsPropsUpdater = new JSPropsUpdater();
export default jsPropsUpdater;
export default JSPropsUpdater;
@@ -1,150 +0,0 @@
'use strict';
import { findNodeHandle } from '../platformFunctions/findNodeHandle';
import { WorkletEventHandler } from '../WorkletEventHandler';
import type {
AnimatedComponentProps,
AnimatedComponentRef,
INativeEventsManager,
InitialComponentProps,
ManagedAnimatedComponent,
} from './commonTypes';
import { has } from './utils';
export class NativeEventsManager implements INativeEventsManager {
readonly #managedComponent: ManagedAnimatedComponent;
readonly #componentOptions?: ComponentOptions;
#eventViewTag = -1;
constructor(component: ManagedAnimatedComponent, options?: ComponentOptions) {
this.#managedComponent = component;
this.#componentOptions = options;
this.#eventViewTag = this.getEventViewTag();
}
public attachEvents() {
executeForEachEventHandler(this.#managedComponent.props, (key, handler) => {
handler.registerForEvents(this.#eventViewTag, key);
});
}
public detachEvents() {
executeForEachEventHandler(
this.#managedComponent.props,
(_key, handler) => {
handler.unregisterFromEvents(this.#eventViewTag);
}
);
}
public updateEvents(
prevProps: AnimatedComponentProps<InitialComponentProps>
) {
const computedEventTag = this.getEventViewTag(true);
// If the event view tag changes, we need to completely re-mount all events
if (this.#eventViewTag !== computedEventTag) {
// Remove all bindings from previous props that ran on the old viewTag
executeForEachEventHandler(prevProps, (_key, handler) => {
handler.unregisterFromEvents(this.#eventViewTag);
});
// We don't need to unregister from current (new) props, because their events weren't registered yet
// Replace the view tag
this.#eventViewTag = computedEventTag;
// Attach the events with a new viewTag
this.attachEvents();
return;
}
executeForEachEventHandler(prevProps, (key, prevHandler) => {
const newProp = this.#managedComponent.props[key];
if (!newProp) {
// Prop got deleted
prevHandler.unregisterFromEvents(this.#eventViewTag);
} else if (
isWorkletEventHandler(newProp) &&
newProp.workletEventHandler !== prevHandler
) {
// Prop got changed
prevHandler.unregisterFromEvents(this.#eventViewTag);
newProp.workletEventHandler.registerForEvents(this.#eventViewTag);
}
});
executeForEachEventHandler(this.#managedComponent.props, (key, handler) => {
if (!prevProps[key]) {
// Prop got added
handler.registerForEvents(this.#eventViewTag);
}
});
}
private getEventViewTag(componentUpdate: boolean = false) {
// Get the tag for registering events - since the event emitting view can be nested inside the main component
const componentAnimatedRef = this.#managedComponent
._componentRef as AnimatedComponentRef & { __nativeTag?: number };
if (componentAnimatedRef?.getScrollableNode) {
/*
In most cases, getScrollableNode() returns a view tag, and findNodeHandle is not required.
However, to cover more exotic list cases, we will continue to use findNodeHandle
for consistency. For numerical values, findNodeHandle should return the value immediately,
as documented here: https://github.com/facebook/react/blob/91061073d57783c061889ac6720ef1ab7f0c2149/packages/react-native-renderer/src/ReactNativePublicCompat.js#L113
*/
const scrollableNode = componentAnimatedRef.getScrollableNode();
if (typeof scrollableNode === 'number') {
return scrollableNode;
}
return findNodeHandle(scrollableNode) ?? -1;
}
if (this.#componentOptions?.setNativeProps) {
// This case ensures backward compatibility with components that
// have their own setNativeProps method passed as an option.
return findNodeHandle(this.#managedComponent) ?? -1;
}
if (!componentUpdate) {
// On the first render of a component, we may already receive a resolved view tag.
return this.#managedComponent.getComponentViewTag();
}
if (componentAnimatedRef?.__nativeTag) {
return componentAnimatedRef.__nativeTag ?? -1;
}
/*
When a component is updated, a child could potentially change and have a different
view tag. This can occur with a GestureDetector component.
*/
return findNodeHandle(componentAnimatedRef) ?? -1;
}
}
function isWorkletEventHandler(
prop: unknown
): prop is WorkletEventHandlerHolder {
return (
has('workletEventHandler', prop) &&
prop.workletEventHandler instanceof WorkletEventHandler
);
}
function executeForEachEventHandler(
props: AnimatedComponentProps<InitialComponentProps>,
callback: (
key: string,
handler: InstanceType<typeof WorkletEventHandler>
) => void
) {
for (const key in props) {
const prop = props[key];
if (isWorkletEventHandler(prop)) {
callback(key, prop.workletEventHandler);
}
}
}
type ComponentOptions = {
setNativeProps?: (
ref: AnimatedComponentRef,
props: InitialComponentProps
) => void;
};
type WorkletEventHandlerHolder = {
workletEventHandler: InstanceType<typeof WorkletEventHandler>;
};
@@ -1,19 +1,19 @@
'use strict';
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 { 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 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,68 +21,56 @@ function dummyListener() {
}
export class PropsFilter implements IPropsFilter {
private _initialPropsMap = new Map<AnimatedStyleHandle, StyleProps>();
private _initialStyle = {};
public filterNonAnimatedProps(
component: AnimatedComponentType
component: React.Component<unknown, unknown> & IAnimatedComponentInternal
): 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?.viewDescriptors) {
const handle = style as AnimatedStyleHandle;
const processedStyle: StyleProps = styles.map((style) => {
if (style && style.viewDescriptors) {
// this is how we recognize styles returned by useAnimatedStyle
style.viewsRef.add(component);
if (component._isFirstRender) {
this._initialPropsMap.set(handle, {
...handle.initial.value,
...initialUpdaterRun(handle.initial.updater),
} as StyleProps);
this._initialStyle = {
...style.initial.value,
...this._initialStyle,
...initialUpdaterRun<StyleProps>(style.initial.updater),
};
}
return this._initialPropsMap.get(handle) ?? {};
return this._initialStyle;
} else if (hasInlineStyles(style)) {
return getInlineStyle(style, component._isFirstRender);
} else {
return style;
}
});
// keep styles as they were passed by the user
// it will help other libs to interpret styles correctly
props[key] = processedStyle;
props[key] = StyleSheet.flatten(processedStyle);
} else if (key === 'animatedProps') {
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];
}
);
}
});
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);
});
}
} else if (
has('workletEventHandler', value) &&
value.workletEventHandler instanceof WorkletEventHandler
has('current', value) &&
value.current instanceof WorkletEventHandler
) {
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]
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]
: dummyListener;
});
} else {
@@ -90,9 +78,9 @@ export class PropsFilter implements IPropsFilter {
}
} else if (isSharedValue(value)) {
if (component._isFirstRender) {
props[key] = value.value;
props[key] = (value as SharedValue<unknown>).value;
}
} else {
} else if (key !== 'onGestureHandlerStateChange' || !isChromeDebugger()) {
props[key] = value;
}
}
@@ -1,35 +1,32 @@
'use strict';
import type { Component, Ref, RefObject } from 'react';
import type { Ref, Component } from 'react';
import type {
AnimatedStyle,
EntryExitAnimationFunction,
ILayoutAnimationBuilder,
ShadowNodeWrapper,
SharedValue,
StyleProps,
StyleUpdaterContainer,
} from '../commonTypes';
import type { SkipEnteringContext } from '../component/LayoutAnimationConfig';
import type { BaseAnimationBuilder } from '../layoutReanimation';
import type { ViewDescriptorsSet } from '../ViewDescriptorsSet';
BaseAnimationBuilder,
ILayoutAnimationBuilder,
EntryExitAnimationFunction,
SharedTransition,
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';
export interface AnimatedProps extends Record<string, unknown> {
viewDescriptors?: ViewDescriptorsSet;
viewsRef?: ViewRefSet<unknown>;
initial?: SharedValue<StyleProps>;
styleUpdaterContainer?: StyleUpdaterContainer;
}
export interface ViewInfo {
viewTag: number | AnimatedComponentRef | HTMLElement | null;
viewTag: number | HTMLElement | null;
viewName: string | null;
shadowNodeWrapper: ShadowNodeWrapper | null;
// 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;
viewConfig: ViewConfig;
}
export interface IInlinePropManager {
@@ -40,49 +37,31 @@ 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: AnimatedComponentType
component: React.Component<unknown, unknown> & IAnimatedComponentInternal
) => Record<string, unknown>;
}
export type JSPropsOperation = {
tag: number;
updates: StyleProps;
};
export interface IJSPropsUpdater {
registerComponent(
animatedComponent: AnimatedComponentType,
jsProps: string[]
addOnJSPropsChangeListener(
animatedComponent: React.Component<unknown, unknown> &
IAnimatedComponentInternal
): void;
removeOnJSPropsChangeListener(
animatedComponent: React.Component<unknown, unknown> &
IAnimatedComponentInternal
): 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> = Record<string, unknown>,
> = P & {
ref?: Ref<Component>;
export type AnimatedComponentProps<P extends Record<string, unknown>> = P & {
forwardedRef?: Ref<Component>;
style?: NestedArray<StyleProps>;
animatedProps?: Partial<AnimatedComponentProps<AnimatedProps>>;
jestAnimatedValues?: RefObject<AnimatedProps>;
animatedStyle?: StyleProps;
layout?: (
| BaseAnimationBuilder
@@ -104,67 +83,34 @@ export type AnimatedComponentProps<
| 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 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>>[];
export interface IAnimatedComponentInternal {
_styles: StyleProps[] | null;
_animatedProps?: Partial<AnimatedComponentProps<AnimatedProps>>;
_viewTag: number;
_isFirstRender: boolean;
jestInlineStyle: NestedArray<StyleProps> | undefined;
jestAnimatedStyle: { value: StyleProps };
jestAnimatedProps: { value: AnimatedProps };
animatedStyle: { value: StyleProps };
_component: AnimatedComponentRef | HTMLElement | null;
_sharedElementTransition: SharedTransition | null;
_jsPropsUpdater: IJSPropsUpdater;
_InlinePropManager: IInlinePropManager;
_PropsFilter: IPropsFilter;
/** Doesn't exist on web. */
_NativeEventsManager?: INativeEventsManager;
_viewInfo?: ViewInfo;
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,24 +1,81 @@
'use strict';
import type {
Component,
ComponentClass,
ComponentType,
FunctionComponent,
Ref,
MutableRefObject,
} from 'react';
import React from 'react';
import type { FlatList, FlatListProps } from 'react-native';
import type { AnimatedProps } from '../helperTypes';
import type { Options } from './AnimatedComponent';
import AnimatedComponentImpl from './AnimatedComponent';
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';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnimatableComponent<C extends ComponentType<any>> = C & {
jsProps?: string[];
const IS_WEB = isWeb();
const IS_FABRIC = isFabric();
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;
};
/**
@@ -29,70 +86,517 @@ type AnimatableComponent<C extends ComponentType<any>> = C & {
* @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: AnimatableComponent<typeof FlatList<unknown>>,
options?: Options<typeof FlatList<unknown>>
): ComponentClass<AnimatedProps<FlatListProps<unknown>>>;
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>>;
export function createAnimatedComponent(
Component: AnimatableComponent<ComponentType<InitialComponentProps>>,
Component: ComponentType<InitialComponentProps>,
options?: Options<InitialComponentProps>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): any {
class AnimatedComponent extends AnimatedComponentImpl {
static displayName = `AnimatedComponent(${
Component.displayName || Component.name || 'Component'
})`;
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>;
constructor(props: AnimatedComponentProps<InitialComponentProps>) {
// 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);
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}
/>
);
}
}
const animatedComponent = (
props: AnimatedComponentProps & { ref: Ref<AnimatedComponent> }
) => {
AnimatedComponent.displayName = `AnimatedComponent(${
Component.displayName || Component.name || 'Component'
})`;
return React.forwardRef<Component>((props, ref) => {
return (
<AnimatedComponent
{...props}
// 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 })}
{...(ref === null ? null : { forwardedRef: ref })}
/>
);
};
animatedComponent.displayName =
Component.displayName || Component.name || 'Component';
return animatedComponent;
});
}
@@ -1,15 +0,0 @@
'use strict';
import type { HostInstance } from '../platform-specific/findHostInstance';
export function getViewInfo(element: HostInstance): {
viewName?: string;
viewTag?: number;
} {
return {
viewName: (element?._viewConfig?.uiViewClassName ??
element?.__internalInstanceHandle?.type ??
element?.__internalInstanceHandle?.elementType) as string,
viewTag: element?.__nativeTag,
};
}
@@ -1,6 +1,4 @@
'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[] {
@@ -25,7 +23,7 @@ export function flattenArray<T>(array: NestedArray<T>): T[] {
export const has = <K extends string>(
key: K,
x: unknown
): x is { [key in K]: unknown } => {
): x is typeof x & { [key in K]: unknown } => {
if (typeof x === 'function' || typeof x === 'object') {
if (x === null || x === undefined) {
return false;
@@ -35,26 +33,3 @@ 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 }
);
}